refactor(renderer): split snapshot state sections

This commit is contained in:
ZacharyZhang-NY committed 2026-04-24 02:48:28 -04:00
1 parent 236714e17a
commit 1383805d66
12 files changed
+1030 -899

No files matched your search

+256
View File
@@ -0,0 +1,256 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { assembleProfileSnapshot } from "@renderer/state/snapshot-sections";
import { useAppStore } from "@renderer/state/store";
import type { AgentRecord, ApprovalRecord, ProfileSnapshot, RunRecord, TaskRecord } from "@shared/types";
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: [],
workProducts: [],
knowledgeBase: [],
sprints: [],
automationRules: [],
agentMessages: [],
workflows: [],
...overrides,
};
}
function expectSnapshotSectionsMatchSnapshot() {
const { snapshot, snapshotSections } = useAppStore.getState();
expect(snapshot).toEqual(snapshotSections ? assembleProfileSnapshot(snapshotSections) : null);
}
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,
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,
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();
});
it("patchTask inserts new task at beginning when task ID is missing", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ tasks: [fakeTask("t-existing")] }));
useAppStore.getState().patchTask(fakeTask("t-new", "New Task"));
const tasks = useAppStore.getState().snapshot!.tasks;
expect(tasks.map((task) => task.id)).toEqual(["t-new", "t-existing"]);
expect(useAppStore.getState().snapshotSections!.company.tasks.map((task) => task.id)).toEqual([
"t-new",
"t-existing",
]);
expectSnapshotSectionsMatchSnapshot();
});
it("patchTask updates existing task in both section and compatibility snapshot", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ tasks: [fakeTask("t-1", "Original")] }));
useAppStore.getState().patchTask(fakeTask("t-1", "Updated"));
expect(useAppStore.getState().snapshot!.tasks[0]!.title).toBe("Updated");
expect(useAppStore.getState().snapshotSections!.company.tasks[0]!.title).toBe("Updated");
expectSnapshotSectionsMatchSnapshot();
});
it("removeTask filters task by ID in both section and compatibility snapshot", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ tasks: [fakeTask("t-1"), fakeTask("t-2")] }));
useAppStore.getState().removeTask("t-1");
expect(useAppStore.getState().snapshot!.tasks.map((task) => task.id)).toEqual(["t-2"]);
expect(useAppStore.getState().snapshotSections!.company.tasks.map((task) => task.id)).toEqual(["t-2"]);
expectSnapshotSectionsMatchSnapshot();
});
it("patchRun inserts and updates run records through snapshot sections", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ runs: [fakeRun("r-existing")] }));
useAppStore.getState().patchRun(fakeRun("r-new"));
useAppStore.getState().patchRun({ ...fakeRun("r-new"), status: "running" });
expect(useAppStore.getState().snapshot!.runs.map((run) => `${run.id}:${run.status}`)).toEqual([
"r-new:running",
"r-existing:queued",
]);
expect(useAppStore.getState().snapshotSections!.company.runs[0]!.status).toBe("running");
expectSnapshotSectionsMatchSnapshot();
});
it("removeRun filters run by ID in both section and compatibility snapshot", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ runs: [fakeRun("r-1"), fakeRun("r-2")] }));
useAppStore.getState().removeRun("r-1");
expect(useAppStore.getState().snapshot!.runs.map((run) => run.id)).toEqual(["r-2"]);
expect(useAppStore.getState().snapshotSections!.company.runs.map((run) => run.id)).toEqual(["r-2"]);
expectSnapshotSectionsMatchSnapshot();
});
it("patchAgent inserts and updates agent records through snapshot sections", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ agents: [fakeAgent("ag-existing")] }));
useAppStore.getState().patchAgent(fakeAgent("ag-new", "New Agent"));
useAppStore.getState().patchAgent(fakeAgent("ag-new", "Renamed Agent"));
expect(useAppStore.getState().snapshot!.agents.map((agent) => `${agent.id}:${agent.name}`)).toEqual([
"ag-new:Renamed Agent",
"ag-existing:Agent",
]);
expect(useAppStore.getState().snapshotSections!.company.agents[0]!.name).toBe("Renamed Agent");
expectSnapshotSectionsMatchSnapshot();
});
it("removeAgent filters agent by ID in both section and compatibility snapshot", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ agents: [fakeAgent("ag-1"), fakeAgent("ag-2")] }));
useAppStore.getState().removeAgent("ag-1");
expect(useAppStore.getState().snapshot!.agents.map((agent) => agent.id)).toEqual(["ag-2"]);
expect(useAppStore.getState().snapshotSections!.company.agents.map((agent) => agent.id)).toEqual(["ag-2"]);
expectSnapshotSectionsMatchSnapshot();
});
it("patchApproval inserts and updates approval records through snapshot sections", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ approvals: [fakeApproval("ap-existing")] }));
useAppStore.getState().patchApproval(fakeApproval("ap-new"));
useAppStore.getState().patchApproval({ ...fakeApproval("ap-new"), state: "approved" });
expect(useAppStore.getState().snapshot!.approvals.map((approval) => `${approval.id}:${approval.state}`)).toEqual([
"ap-new:approved",
"ap-existing:pending",
]);
expect(useAppStore.getState().snapshotSections!.company.approvals[0]!.state).toBe("approved");
expectSnapshotSectionsMatchSnapshot();
});
it("removeApproval filters approval by ID in both section and compatibility snapshot", () => {
useAppStore.getState().setSnapshot(makeSnapshot({ approvals: [fakeApproval("ap-1"), fakeApproval("ap-2")] }));
useAppStore.getState().removeApproval("ap-1");
expect(useAppStore.getState().snapshot!.approvals.map((approval) => approval.id)).toEqual(["ap-2"]);
expect(useAppStore.getState().snapshotSections!.company.approvals.map((approval) => approval.id)).toEqual([
"ap-2",
]);
expectSnapshotSectionsMatchSnapshot();
});
it("patch methods leave null snapshot state unchanged", () => {
expect(useAppStore.getState().snapshot).toBeNull();
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();
expect(useAppStore.getState().snapshotSections).toBeNull();
});
});
-181
View File
@@ -1,181 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { createDomainEventBatcher, type GranularEventEntry } from "@renderer/lib/ipc-batcher";
import type { DesktopEvent } from "@shared/types";
describe("createDomainEventBatcher", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("domain-changed event triggers full refresh callback after 50ms window", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
const domainEvent: DesktopEvent = { type: "domain-changed" };
batcher.push(domainEvent);
expect(onFull).not.toHaveBeenCalled();
vi.advanceTimersByTime(50);
expect(onFull).toHaveBeenCalledTimes(1);
expect(onGranular).not.toHaveBeenCalled();
});
it("multiple domain-changed events in same window coalesce to single full refresh", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "domain-changed" } as DesktopEvent);
batcher.push({ type: "domain-changed" } as DesktopEvent);
batcher.push({ type: "domain-changed" } as DesktopEvent);
vi.advanceTimersByTime(50);
expect(onFull).toHaveBeenCalledTimes(1);
});
it("single task-updated event triggers granular batch callback with one entry after 50ms", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
const taskEvent: DesktopEvent = { type: "task-updated", taskId: "t-1", action: "updated" };
batcher.push(taskEvent);
vi.advanceTimersByTime(50);
expect(onGranular).toHaveBeenCalledTimes(1);
expect(onFull).not.toHaveBeenCalled();
const entries: GranularEventEntry[] = onGranular.mock.calls[0]![0];
expect(entries).toHaveLength(1);
expect(entries[0]).toEqual({ type: "task-updated", entityId: "t-1", action: "updated" });
});
it("multiple task-updated events for SAME taskId coalesce to single entry (last-write-wins on action)", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "task-updated", taskId: "t-1", action: "created" } as DesktopEvent);
batcher.push({ type: "task-updated", taskId: "t-1", action: "updated" } as DesktopEvent);
vi.advanceTimersByTime(50);
const entries: GranularEventEntry[] = onGranular.mock.calls[0]![0];
expect(entries).toHaveLength(1);
expect(entries[0]!.action).toBe("updated");
});
it("multiple task-updated events for DIFFERENT taskIds produce separate entries", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "task-updated", taskId: "t-1", action: "updated" } as DesktopEvent);
batcher.push({ type: "task-updated", taskId: "t-2", action: "created" } as DesktopEvent);
vi.advanceTimersByTime(50);
const entries: GranularEventEntry[] = onGranular.mock.calls[0]![0];
expect(entries).toHaveLength(2);
});
it("domain-changed in same window as granular events causes full refresh (domain-changed overrides)", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "task-updated", taskId: "t-1", action: "updated" } as DesktopEvent);
batcher.push({ type: "run-updated", runId: "r-1", action: "completed" } as DesktopEvent);
batcher.push({ type: "domain-changed" } as DesktopEvent);
vi.advanceTimersByTime(50);
expect(onFull).toHaveBeenCalledTimes(1);
expect(onGranular).not.toHaveBeenCalled();
});
it("more than 10 unique granular entities in one window triggers full refresh instead of granular batch", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
// Push 11 unique task-updated events
for (let i = 0; i < 11; i++) {
batcher.push({ type: "task-updated", taskId: `t-${i}`, action: "updated" } as DesktopEvent);
}
vi.advanceTimersByTime(50);
expect(onFull).toHaveBeenCalledTimes(1);
expect(onGranular).not.toHaveBeenCalled();
});
it("flush() immediately processes pending events", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "task-updated", taskId: "t-1", action: "created" } as DesktopEvent);
batcher.flush();
expect(onGranular).toHaveBeenCalledTimes(1);
// Should not fire again on timer
vi.advanceTimersByTime(100);
expect(onGranular).toHaveBeenCalledTimes(1);
});
it("dispose() cancels pending events without firing callbacks", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "task-updated", taskId: "t-1", action: "created" } as DesktopEvent);
batcher.dispose();
vi.advanceTimersByTime(100);
expect(onFull).not.toHaveBeenCalled();
expect(onGranular).not.toHaveBeenCalled();
});
it("run-log, shortcut, notification-click events are NOT handled by batcher (treated as unknown, triggers full refresh)", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
// These events are not recognized as granular -- per D-05, unknown triggers full refresh
batcher.push({ type: "run-log", runId: "r-1", stream: "stdout", chunk: "hi" } as DesktopEvent);
vi.advanceTimersByTime(50);
expect(onFull).toHaveBeenCalledTimes(1);
expect(onGranular).not.toHaveBeenCalled();
});
it("budget-changed event appears in granular batch with companyId as entity key", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "budget-changed", companyId: "c-1" } as DesktopEvent);
vi.advanceTimersByTime(50);
expect(onGranular).toHaveBeenCalledTimes(1);
const entries: GranularEventEntry[] = onGranular.mock.calls[0]![0];
expect(entries).toHaveLength(1);
expect(entries[0]).toEqual({ type: "budget-changed", entityId: "c-1", action: "changed" });
});
it("if any event in window has action 'deleted', that action survives coalescing for that entity", () => {
const onFull = vi.fn();
const onGranular = vi.fn();
const batcher = createDomainEventBatcher(onFull, onGranular, 50);
batcher.push({ type: "task-updated", taskId: "t-1", action: "deleted" } as DesktopEvent);
batcher.push({ type: "task-updated", taskId: "t-1", action: "updated" } as DesktopEvent);
vi.advanceTimersByTime(50);
const entries: GranularEventEntry[] = onGranular.mock.calls[0]![0];
expect(entries).toHaveLength(1);
expect(entries[0]!.action).toBe("deleted");
});
});
+75 -251
View File
@@ -1,4 +1,4 @@
import { describe, it, expect, vi } from "vitest";
import { describe, it, expect } from "vitest";
import type {
DesktopEvent,
TaskUpdatedEvent,
@@ -7,7 +7,9 @@ import type {
AgentUpdatedEvent,
BudgetChangedEvent,
} from "@shared/types";
import type { GranularEventEntry } from "@renderer/lib/ipc-batcher";
import {
classifySnapshotEvent,
} from "@renderer/lib/snapshot-events";
describe("selective IPC event types", () => {
it("TaskUpdatedEvent discriminated by type field", () => {
@@ -63,7 +65,7 @@ describe("selective IPC event types", () => {
expect(event.companyId).toBe("c-1");
});
it("all five new event types are assignable to DesktopEvent union", () => {
it("all five entity event types are assignable to DesktopEvent union", () => {
const events: DesktopEvent[] = [
{ type: "task-updated", taskId: "t-1", action: "created" },
{ type: "run-updated", runId: "r-1", action: "status_changed" },
@@ -74,8 +76,7 @@ describe("selective IPC event types", () => {
expect(events).toHaveLength(5);
});
it("DesktopEvent union has all 14 members (9 original + 5 new)", () => {
// This is a compile-time check -- if any type is missing, TypeScript will error
it("DesktopEvent union has all members used by snapshot classification", () => {
const exhaustiveCheck = (event: DesktopEvent): string => {
switch (event.type) {
case "run-log":
@@ -130,262 +131,85 @@ describe("selective IPC event types", () => {
}
}
};
// Dummy call to avoid unused-var warning
expect(typeof exhaustiveCheck).toBe("function");
});
});
// Extracted granular batch handler logic for testability
// Mirrors the handleGranularBatch function in App.tsx
async function handleGranularBatch(
events: GranularEventEntry[],
deps: {
companyId: string | null;
getTask: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
getRun: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
getApproval: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
getAgent: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
patchTask: (task: unknown) => void;
removeTask: (id: string) => void;
patchRun: (run: unknown) => void;
removeRun: (id: string) => void;
patchApproval: (approval: unknown) => void;
removeApproval: (id: string) => void;
patchAgent: (agent: unknown) => void;
refreshBadges: () => Promise<void>;
fullRefresh: () => void;
},
) {
if (!deps.companyId) {
deps.fullRefresh();
return;
}
describe("classifySnapshotEvent", () => {
it("maps direct entity events to one immediate entity decision", () => {
expect(classifySnapshotEvent({ type: "task-updated", taskId: "t-1", action: "updated" })).toEqual({
kind: "entity",
entry: { type: "task-updated", entityId: "t-1", action: "updated" },
});
expect(classifySnapshotEvent({ type: "run-updated", runId: "r-1", action: "status_changed" })).toEqual({
kind: "entity",
entry: { type: "run-updated", entityId: "r-1", action: "status_changed" },
});
expect(classifySnapshotEvent({ type: "approval-updated", approvalId: "a-1", action: "decided" })).toEqual({
kind: "entity",
entry: { type: "approval-updated", entityId: "a-1", action: "decided" },
});
expect(classifySnapshotEvent({ type: "agent-updated", agentId: "ag-1", action: "paused" })).toEqual({
kind: "entity",
entry: { type: "agent-updated", entityId: "ag-1", action: "paused" },
});
expect(classifySnapshotEvent({ type: "budget-changed", companyId: "company-1" })).toEqual({
kind: "entity",
entry: { type: "budget-changed", entityId: "company-1", action: "changed" },
});
});
let needsFullRefresh = false;
it("maps legacy progress events to their owning entities", () => {
expect(classifySnapshotEvent({ type: "run-status", runId: "r-1", status: "running", message: "Running" })).toEqual({
kind: "entity",
entry: { type: "run-updated", entityId: "r-1", action: "status_changed" },
});
expect(classifySnapshotEvent({ type: "heartbeat-status", agentId: "ag-1", enabled: true, nextWakeAt: null })).toEqual({
kind: "entity",
entry: { type: "agent-updated", entityId: "ag-1", action: "updated" },
});
expect(classifySnapshotEvent({ type: "agent-message", taskId: "t-1", comment: {} } as DesktopEvent)).toEqual({
kind: "entity",
entry: { type: "task-updated", entityId: "t-1", action: "updated" },
});
});
for (const entry of events) {
if (entry.type === "run-updated" && entry.action === "completed") {
needsFullRefresh = true;
break;
it("routes domain-wide events to full refresh", () => {
const events: DesktopEvent[] = [
{ type: "domain-changed" },
{ type: "new-message" } as DesktopEvent,
{ type: "thread-wait-state-updated", threadId: "th-1", companyId: "c-1", waitStateId: "w-1", action: "created" },
{ type: "credentials-changed", connectorId: "claude-pro" },
{ type: "agent-event", runId: "r-1", agentId: "ag-1", companyId: "c-1", taskId: "t-1", emittedAt: "2026-01-01", event: { type: "x" } },
{ type: "oauth:status", connectorId: "claude-pro", status: "active" },
{ type: "oauth:auth-url", connectorId: "claude-pro", url: "https://example.com" },
{ type: "oauth:progress", connectorId: "claude-pro", message: "Working" },
{ type: "oauth:prompt-request", connectorId: "claude-pro", requestId: "req-1", message: "Code" },
{ type: "oauth:manual-code-request", connectorId: "claude-pro", requestId: "req-1" },
];
for (const event of events) {
expect(classifySnapshotEvent(event)).toEqual({ kind: "full-refresh" });
}
});
try {
switch (entry.type) {
case "task-updated": {
if (entry.action === "deleted") {
deps.removeTask(entry.entityId);
} else {
const result = await deps.getTask({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchTask(result.data);
} else {
deps.removeTask(entry.entityId);
}
}
break;
}
case "run-updated": {
if (entry.action === "deleted") {
deps.removeRun(entry.entityId);
} else {
const result = await deps.getRun({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchRun(result.data);
} else {
deps.removeRun(entry.entityId);
}
}
break;
}
case "approval-updated": {
if (entry.action === "deleted") {
deps.removeApproval(entry.entityId);
} else {
const result = await deps.getApproval({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchApproval(result.data);
} else {
deps.removeApproval(entry.entityId);
}
}
break;
}
case "agent-updated": {
const result = await deps.getAgent({ companyId: deps.companyId, id: entry.entityId });
if (result.ok) {
deps.patchAgent(result.data);
} else {
needsFullRefresh = true;
}
break;
}
case "budget-changed": {
needsFullRefresh = true;
break;
}
default: {
needsFullRefresh = true;
break;
}
}
} catch {
needsFullRefresh = true;
break;
it("ignores events with independent listeners", () => {
const events: DesktopEvent[] = [
{ type: "automation-fired", ruleId: "rule-1", ruleName: "Rule", trigger: "schedule", action: "run_task" },
{ type: "recovery-needed", interruptedRuns: [] },
{ type: "run-log", runId: "r-1", stream: "stdout", chunk: "chunk" },
{ type: "shortcut", action: "refresh" },
{ type: "notification-click", section: "tasks", entityId: "t-1" },
];
for (const event of events) {
expect(classifySnapshotEvent(event)).toEqual({ kind: "ignore" });
}
if (needsFullRefresh) break;
}
if (needsFullRefresh) {
deps.fullRefresh();
} else {
void deps.refreshBadges();
}
}
function createMockDeps() {
return {
companyId: "company-1" as string | null,
getTask: vi.fn().mockResolvedValue({ ok: true, data: { id: "t-1", title: "Test task" } }),
getRun: vi.fn().mockResolvedValue({ ok: true, data: { id: "r-1", status: "running" } }),
getApproval: vi.fn().mockResolvedValue({ ok: true, data: { id: "a-1", state: "pending" } }),
getAgent: vi.fn().mockResolvedValue({ ok: true, data: { id: "ag-1", name: "Agent" } }),
patchTask: vi.fn(),
removeTask: vi.fn(),
patchRun: vi.fn(),
removeRun: vi.fn(),
patchApproval: vi.fn(),
removeApproval: vi.fn(),
patchAgent: vi.fn(),
refreshBadges: vi.fn().mockResolvedValue(undefined),
fullRefresh: vi.fn(),
};
}
describe("handleGranularBatch", () => {
it("task-updated event calls getTask and patchTask", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.getTask).toHaveBeenCalledWith({ companyId: "company-1", id: "t-1" });
expect(deps.patchTask).toHaveBeenCalledWith({ id: "t-1", title: "Test task" });
expect(deps.fullRefresh).not.toHaveBeenCalled();
expect(deps.refreshBadges).toHaveBeenCalled();
});
it("deleted task calls removeTask without fetching", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "deleted" }],
deps,
);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.getTask).not.toHaveBeenCalled();
expect(deps.fullRefresh).not.toHaveBeenCalled();
});
it("run-updated:completed triggers full refresh", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "run-updated", entityId: "r-1", action: "completed" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.getRun).not.toHaveBeenCalled();
expect(deps.refreshBadges).not.toHaveBeenCalled();
});
it("failed getTask fetch falls back to removeTask", async () => {
const deps = createMockDeps();
deps.getTask.mockResolvedValue({ ok: false, error: { code: "NOT_FOUND", message: "nope" } });
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.patchTask).not.toHaveBeenCalled();
});
it("calls refreshBadges after successful granular processing", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "created" }],
deps,
);
expect(deps.refreshBadges).toHaveBeenCalled();
expect(deps.fullRefresh).not.toHaveBeenCalled();
});
it("approval-updated event calls getApproval and patchApproval", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "approval-updated", entityId: "a-1", action: "decided" }],
deps,
);
expect(deps.getApproval).toHaveBeenCalledWith({ companyId: "company-1", id: "a-1" });
expect(deps.patchApproval).toHaveBeenCalledWith({ id: "a-1", state: "pending" });
});
it("agent-updated event calls getAgent and patchAgent", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "agent-updated", entityId: "ag-1", action: "paused" }],
deps,
);
expect(deps.getAgent).toHaveBeenCalledWith({ companyId: "company-1", id: "ag-1" });
expect(deps.patchAgent).toHaveBeenCalledWith({ id: "ag-1", name: "Agent" });
});
it("budget-changed triggers full refresh", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "budget-changed", entityId: "c-1", action: "changed" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.refreshBadges).not.toHaveBeenCalled();
});
it("no companyId triggers full refresh", async () => {
const deps = createMockDeps();
deps.companyId = null;
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.getTask).not.toHaveBeenCalled();
});
it("IPC error falls back to full refresh", async () => {
const deps = createMockDeps();
deps.getTask.mockRejectedValue(new Error("IPC failed"));
await handleGranularBatch(
[{ type: "task-updated", entityId: "t-1", action: "updated" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("unknown event type triggers full refresh", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "some-unknown-event", entityId: "x-1", action: "created" }],
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("run-updated:status_changed fetches and patches run", async () => {
const deps = createMockDeps();
await handleGranularBatch(
[{ type: "run-updated", entityId: "r-1", action: "status_changed" }],
deps,
);
expect(deps.getRun).toHaveBeenCalledWith({ companyId: "company-1", id: "r-1" });
expect(deps.patchRun).toHaveBeenCalledWith({ id: "r-1", status: "running" });
expect(deps.fullRefresh).not.toHaveBeenCalled();
it("routes unknown runtime event shapes to full refresh", () => {
expect(classifySnapshotEvent({ type: "future-event" } as unknown as DesktopEvent)).toEqual({
kind: "full-refresh",
});
});
});
+345
View File
@@ -0,0 +1,345 @@
import { describe, expect, it, vi } from "vitest";
import {
createSnapshotEventGuard,
type GranularEventEntry,
type SnapshotEventGuard,
} from "@renderer/lib/snapshot-events";
async function handleSnapshotEntityEvent(
entry: GranularEventEntry,
deps: {
companyId: string | null;
getTask: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
getRun: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
getApproval: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
getAgent: (input: { companyId: string; id: string }) => Promise<{ ok: boolean; data?: unknown }>;
patchTask: (task: unknown) => void;
removeTask: (id: string) => void;
patchRun: (run: unknown) => void;
removeRun: (id: string) => void;
patchApproval: (approval: unknown) => void;
removeApproval: (id: string) => void;
patchAgent: (agent: unknown) => void;
fullRefresh: () => void;
},
eventGuard: SnapshotEventGuard = createSnapshotEventGuard(),
) {
function requestFullRefresh() {
eventGuard.invalidateSnapshot();
deps.fullRefresh();
}
if (!deps.companyId) {
requestFullRefresh();
return;
}
let needsFullRefresh = false;
eventGuard.invalidateSnapshot();
eventGuard.invalidateEntity(entry);
const token = eventGuard.captureEntity(entry);
if (entry.type === "run-updated" && entry.action === "completed") {
needsFullRefresh = true;
} else {
try {
switch (entry.type) {
case "task-updated": {
if (entry.action === "deleted") {
deps.removeTask(entry.entityId);
} else {
const result = await deps.getTask({ companyId: deps.companyId, id: entry.entityId });
if (!eventGuard.canApplyEntity(entry, token)) return;
if (result.ok) {
deps.patchTask(result.data);
} else {
deps.removeTask(entry.entityId);
}
}
break;
}
case "run-updated": {
if (entry.action === "deleted") {
deps.removeRun(entry.entityId);
} else {
const result = await deps.getRun({ companyId: deps.companyId, id: entry.entityId });
if (!eventGuard.canApplyEntity(entry, token)) return;
if (result.ok) {
deps.patchRun(result.data);
} else {
deps.removeRun(entry.entityId);
}
}
break;
}
case "approval-updated": {
if (entry.action === "deleted") {
deps.removeApproval(entry.entityId);
} else {
const result = await deps.getApproval({ companyId: deps.companyId, id: entry.entityId });
if (!eventGuard.canApplyEntity(entry, token)) return;
if (result.ok) {
deps.patchApproval(result.data);
} else {
deps.removeApproval(entry.entityId);
}
}
break;
}
case "agent-updated": {
const result = await deps.getAgent({ companyId: deps.companyId, id: entry.entityId });
if (!eventGuard.canApplyEntity(entry, token)) return;
if (result.ok) {
deps.patchAgent(result.data);
} else {
needsFullRefresh = true;
}
break;
}
case "budget-changed": {
needsFullRefresh = true;
break;
}
default: {
needsFullRefresh = true;
break;
}
}
} catch {
needsFullRefresh = true;
}
}
if (needsFullRefresh) {
requestFullRefresh();
}
}
function createMockDeps() {
return {
companyId: "company-1" as string | null,
getTask: vi.fn().mockResolvedValue({ ok: true, data: { id: "t-1", title: "Test task" } }),
getRun: vi.fn().mockResolvedValue({ ok: true, data: { id: "r-1", status: "running" } }),
getApproval: vi.fn().mockResolvedValue({ ok: true, data: { id: "a-1", state: "pending" } }),
getAgent: vi.fn().mockResolvedValue({ ok: true, data: { id: "ag-1", name: "Agent" } }),
patchTask: vi.fn(),
removeTask: vi.fn(),
patchRun: vi.fn(),
removeRun: vi.fn(),
patchApproval: vi.fn(),
removeApproval: vi.fn(),
patchAgent: vi.fn(),
fullRefresh: vi.fn(),
};
}
function deferred<T>() {
let resolve!: (value: T) => void;
const promise = new Promise<T>((innerResolve) => {
resolve = innerResolve;
});
return { promise, resolve };
}
describe("snapshot entity guard", () => {
it("task-updated event calls getTask and patchTask", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
);
expect(deps.getTask).toHaveBeenCalledWith({ companyId: "company-1", id: "t-1" });
expect(deps.patchTask).toHaveBeenCalledWith({ id: "t-1", title: "Test task" });
expect(deps.fullRefresh).toHaveBeenCalledTimes(0);
});
it("deleted task calls removeTask without fetching", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "deleted" },
deps,
);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.getTask).toHaveBeenCalledTimes(0);
expect(deps.fullRefresh).toHaveBeenCalledTimes(0);
});
it("task delete invalidates a pending full snapshot refresh token", async () => {
const deps = createMockDeps();
const eventGuard = createSnapshotEventGuard();
eventGuard.invalidateSnapshot();
const pendingRefresh = eventGuard.captureSnapshot();
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "deleted" },
deps,
eventGuard,
);
expect(eventGuard.canApplySnapshot(pendingRefresh)).toBe(false);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
});
it("delayed task update cannot resurrect a task deleted by a later event", async () => {
const deps = createMockDeps();
const eventGuard = createSnapshotEventGuard();
const taskFetch = deferred<{ ok: boolean; data: { id: string; title: string } }>();
deps.getTask.mockReturnValue(taskFetch.promise);
const update = handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
eventGuard,
);
expect(deps.getTask).toHaveBeenCalledWith({ companyId: "company-1", id: "t-1" });
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "deleted" },
deps,
eventGuard,
);
taskFetch.resolve({ ok: true, data: { id: "t-1", title: "Stale task" } });
await update;
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.patchTask).toHaveBeenCalledTimes(0);
});
it("newer full refresh invalidates a delayed task update", async () => {
const deps = createMockDeps();
const eventGuard = createSnapshotEventGuard();
const taskFetch = deferred<{ ok: boolean; data: { id: string; title: string } }>();
deps.getTask.mockReturnValue(taskFetch.promise);
const update = handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
eventGuard,
);
eventGuard.invalidateSnapshot();
taskFetch.resolve({ ok: true, data: { id: "t-1", title: "Stale task" } });
await update;
expect(deps.patchTask).toHaveBeenCalledTimes(0);
});
it("later task update wins over an earlier delayed task update", async () => {
const deps = createMockDeps();
const eventGuard = createSnapshotEventGuard();
const firstFetch = deferred<{ ok: boolean; data: { id: string; title: string } }>();
const secondFetch = deferred<{ ok: boolean; data: { id: string; title: string } }>();
deps.getTask
.mockReturnValueOnce(firstFetch.promise)
.mockReturnValueOnce(secondFetch.promise);
const firstUpdate = handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
eventGuard,
);
const secondUpdate = handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
eventGuard,
);
secondFetch.resolve({ ok: true, data: { id: "t-1", title: "Fresh task" } });
await secondUpdate;
firstFetch.resolve({ ok: true, data: { id: "t-1", title: "Stale task" } });
await firstUpdate;
expect(deps.patchTask).toHaveBeenCalledTimes(1);
expect(deps.patchTask).toHaveBeenCalledWith({ id: "t-1", title: "Fresh task" });
});
it("run-updated completed triggers full refresh", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "run-updated", entityId: "r-1", action: "completed" },
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.getRun).toHaveBeenCalledTimes(0);
});
it("failed getTask fetch removes the missing task", async () => {
const deps = createMockDeps();
deps.getTask.mockResolvedValue({ ok: false, error: { code: "NOT_FOUND", message: "missing" } });
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
);
expect(deps.removeTask).toHaveBeenCalledWith("t-1");
expect(deps.patchTask).toHaveBeenCalledTimes(0);
});
it("approval-updated event calls getApproval and patchApproval", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "approval-updated", entityId: "a-1", action: "decided" },
deps,
);
expect(deps.getApproval).toHaveBeenCalledWith({ companyId: "company-1", id: "a-1" });
expect(deps.patchApproval).toHaveBeenCalledWith({ id: "a-1", state: "pending" });
});
it("agent-updated event calls getAgent and patchAgent", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "agent-updated", entityId: "ag-1", action: "paused" },
deps,
);
expect(deps.getAgent).toHaveBeenCalledWith({ companyId: "company-1", id: "ag-1" });
expect(deps.patchAgent).toHaveBeenCalledWith({ id: "ag-1", name: "Agent" });
});
it("budget-changed triggers full refresh", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "budget-changed", entityId: "c-1", action: "changed" },
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("missing companyId triggers full refresh", async () => {
const deps = createMockDeps();
deps.companyId = null;
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
expect(deps.getTask).toHaveBeenCalledTimes(0);
});
it("IPC error triggers full refresh", async () => {
const deps = createMockDeps();
deps.getTask.mockRejectedValue(new Error("IPC failed"));
await handleSnapshotEntityEvent(
{ type: "task-updated", entityId: "t-1", action: "updated" },
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("unknown entity event triggers full refresh", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "future-entity", entityId: "x-1", action: "created" } as unknown as GranularEventEntry,
deps,
);
expect(deps.fullRefresh).toHaveBeenCalled();
});
it("run-updated status change fetches and patches run", async () => {
const deps = createMockDeps();
await handleSnapshotEntityEvent(
{ type: "run-updated", entityId: "r-1", action: "status_changed" },
deps,
);
expect(deps.getRun).toHaveBeenCalledWith({ companyId: "company-1", id: "r-1" });
expect(deps.patchRun).toHaveBeenCalledWith({ id: "r-1", status: "running" });
expect(deps.fullRefresh).toHaveBeenCalledTimes(0);
});
});
+12 -283
View File
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useAppStore } from "@renderer/state/store";
import type { ProfileSnapshot, TaskRecord, RunRecord, AgentRecord, ApprovalRecord } from "@shared/types";
import { assembleProfileSnapshot } from "@renderer/state/snapshot-sections";
import type { ProfileSnapshot } from "@shared/types";
// Mock window.agentCompany for refreshSnapshot tests. Sidebar badges
// no longer traverse IPC — they are derived from the snapshot in
@@ -31,6 +32,7 @@ describe("useAppStore", () => {
// CompanySlice
expect(state).toHaveProperty("snapshot");
expect(state).toHaveProperty("snapshotSections");
expect(state).toHaveProperty("setSnapshot");
expect(state).toHaveProperty("refreshSnapshot");
@@ -149,6 +151,10 @@ describe("useAppStore", () => {
useAppStore.getState().setSnapshot(mockSnapshot);
expect(useAppStore.getState().snapshot).toEqual(mockSnapshot);
expect(useAppStore.getState().snapshotSections?.metadata.profilePath).toBe("/test/path");
expect(useAppStore.getState().snapshotSections?.catalog.companies).toEqual([]);
expect(useAppStore.getState().snapshotSections?.company.tasks).toEqual([]);
expectSnapshotSectionsMatchSnapshot();
});
it("CompanySlice.refreshSnapshot() calls window.agentCompany.loadSnapshot()", async () => {
@@ -189,6 +195,8 @@ describe("useAppStore", () => {
expect(mockLoadSnapshot).toHaveBeenCalledTimes(1);
expect(useAppStore.getState().snapshot).toEqual(mockSnapshot);
expect(useAppStore.getState().snapshotSections?.metadata.currentCompanyId).toBe("company-1");
expectSnapshotSectionsMatchSnapshot();
});
it("UiSlice.setLocale('zh') updates locale to 'zh'", () => {
@@ -224,286 +232,7 @@ describe("useAppStore", () => {
});
});
// -- 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: [],
workProducts: [],
knowledgeBase: [],
sprints: [],
automationRules: [],
agentMessages: [],
workflows: [],
...overrides,
};
function expectSnapshotSectionsMatchSnapshot() {
const { snapshot, snapshotSections } = useAppStore.getState();
expect(snapshot).toEqual(snapshotSections ? assembleProfileSnapshot(snapshotSections) : null);
}
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,
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,
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();
});
});