216 lines
7.9 KiB
TypeScript
216 lines
7.9 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import type {
|
|
DesktopEvent,
|
|
TaskUpdatedEvent,
|
|
RunUpdatedEvent,
|
|
ApprovalUpdatedEvent,
|
|
AgentUpdatedEvent,
|
|
BudgetChangedEvent,
|
|
} from "@shared/types";
|
|
import {
|
|
classifySnapshotEvent,
|
|
} from "@renderer/lib/snapshot-events";
|
|
|
|
describe("selective IPC event types", () => {
|
|
it("TaskUpdatedEvent discriminated by type field", () => {
|
|
const event: TaskUpdatedEvent = {
|
|
type: "task-updated",
|
|
taskId: "t-1",
|
|
action: "created",
|
|
};
|
|
expect(event.type).toBe("task-updated");
|
|
expect(event.taskId).toBe("t-1");
|
|
expect(event.action).toBe("created");
|
|
});
|
|
|
|
it("RunUpdatedEvent discriminated by type field", () => {
|
|
const event: RunUpdatedEvent = {
|
|
type: "run-updated",
|
|
runId: "r-1",
|
|
action: "completed",
|
|
};
|
|
expect(event.type).toBe("run-updated");
|
|
expect(event.runId).toBe("r-1");
|
|
expect(event.action).toBe("completed");
|
|
});
|
|
|
|
it("ApprovalUpdatedEvent discriminated by type field", () => {
|
|
const event: ApprovalUpdatedEvent = {
|
|
type: "approval-updated",
|
|
approvalId: "a-1",
|
|
action: "decided",
|
|
};
|
|
expect(event.type).toBe("approval-updated");
|
|
expect(event.approvalId).toBe("a-1");
|
|
expect(event.action).toBe("decided");
|
|
});
|
|
|
|
it("AgentUpdatedEvent discriminated by type field", () => {
|
|
const event: AgentUpdatedEvent = {
|
|
type: "agent-updated",
|
|
agentId: "ag-1",
|
|
action: "updated",
|
|
};
|
|
expect(event.type).toBe("agent-updated");
|
|
expect(event.agentId).toBe("ag-1");
|
|
expect(event.action).toBe("updated");
|
|
});
|
|
|
|
it("BudgetChangedEvent discriminated by type field", () => {
|
|
const event: BudgetChangedEvent = {
|
|
type: "budget-changed",
|
|
companyId: "c-1",
|
|
};
|
|
expect(event.type).toBe("budget-changed");
|
|
expect(event.companyId).toBe("c-1");
|
|
});
|
|
|
|
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" },
|
|
{ type: "approval-updated", approvalId: "a-1", action: "created" },
|
|
{ type: "agent-updated", agentId: "ag-1", action: "paused" },
|
|
{ type: "budget-changed", companyId: "c-1" },
|
|
];
|
|
expect(events).toHaveLength(5);
|
|
});
|
|
|
|
it("DesktopEvent union has all members used by snapshot classification", () => {
|
|
const exhaustiveCheck = (event: DesktopEvent): string => {
|
|
switch (event.type) {
|
|
case "run-log":
|
|
return "run-log";
|
|
case "domain-changed":
|
|
return "domain-changed";
|
|
case "shortcut":
|
|
return "shortcut";
|
|
case "run-status":
|
|
return "run-status";
|
|
case "heartbeat-status":
|
|
return "heartbeat-status";
|
|
case "agent-message":
|
|
return "agent-message";
|
|
case "automation-fired":
|
|
return "automation-fired";
|
|
case "new-message":
|
|
return "new-message";
|
|
case "notification-click":
|
|
return "notification-click";
|
|
case "task-updated":
|
|
return "task-updated";
|
|
case "run-updated":
|
|
return "run-updated";
|
|
case "approval-updated":
|
|
return "approval-updated";
|
|
case "agent-updated":
|
|
return "agent-updated";
|
|
case "budget-changed":
|
|
return "budget-changed";
|
|
case "thread-wait-state-updated":
|
|
return "thread-wait-state-updated";
|
|
case "recovery-needed":
|
|
return "recovery-needed";
|
|
case "oauth:status":
|
|
return "oauth:status";
|
|
case "oauth:auth-url":
|
|
return "oauth:auth-url";
|
|
case "oauth:progress":
|
|
return "oauth:progress";
|
|
case "oauth:prompt-request":
|
|
return "oauth:prompt-request";
|
|
case "oauth:manual-code-request":
|
|
return "oauth:manual-code-request";
|
|
case "credentials-changed":
|
|
return "credentials-changed";
|
|
case "agent-event":
|
|
return "agent-event";
|
|
default: {
|
|
const _exhaustive: never = event;
|
|
return _exhaustive;
|
|
}
|
|
}
|
|
};
|
|
expect(typeof exhaustiveCheck).toBe("function");
|
|
});
|
|
});
|
|
|
|
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" },
|
|
});
|
|
});
|
|
|
|
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" },
|
|
});
|
|
});
|
|
|
|
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" });
|
|
}
|
|
});
|
|
|
|
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" });
|
|
}
|
|
});
|
|
|
|
it("routes unknown runtime event shapes to full refresh", () => {
|
|
expect(classifySnapshotEvent({ type: "future-event" } as unknown as DesktopEvent)).toEqual({
|
|
kind: "full-refresh",
|
|
});
|
|
});
|
|
});
|