test: add coverage for terminalNotifier, testNotifications, and claude store

This commit is contained in:
2026-03-03 15:17:29 -08:00
committed by Naomi Carrigan
parent c819adc9ea
commit a5fb22bd23
3 changed files with 261 additions and 0 deletions
@@ -0,0 +1,64 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { NotificationType } from "./types";
import { claudeStore } from "$lib/stores/claude";
import { sendTerminalNotification } from "./terminalNotifier";
vi.mock("$lib/stores/claude", () => ({
claudeStore: {
addLine: vi.fn(),
},
}));
describe("sendTerminalNotification", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("adds a system line for success type with sparkle emoji", () => {
sendTerminalNotification(NotificationType.SUCCESS);
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("✨"));
});
it("adds a system line for error type with cross emoji", () => {
sendTerminalNotification(NotificationType.ERROR);
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("❌"));
});
it("adds a system line for permission type with lock emoji", () => {
sendTerminalNotification(NotificationType.PERMISSION);
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("🔐"));
});
it("adds a system line for connection type with link emoji", () => {
sendTerminalNotification(NotificationType.CONNECTION);
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("🔗"));
});
it("adds a system line for task_start type with rocket emoji", () => {
sendTerminalNotification(NotificationType.TASK_START);
expect(claudeStore.addLine).toHaveBeenCalledWith("system", expect.stringContaining("🚀"));
});
it("includes the optional message in the notification", () => {
sendTerminalNotification(NotificationType.SUCCESS, "Custom message text");
expect(claudeStore.addLine).toHaveBeenCalledWith(
"system",
expect.stringContaining("Custom message text")
);
});
it("includes the sound phrase as the notification title", () => {
sendTerminalNotification(NotificationType.SUCCESS);
expect(claudeStore.addLine).toHaveBeenCalledWith(
"system",
expect.stringContaining("I'm done!")
);
});
});
@@ -0,0 +1,58 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { testAllNotifications } from "./testNotifications";
vi.mock("./notificationManager", () => ({
notificationManager: {
notifySuccess: vi.fn().mockResolvedValue(undefined),
notifyError: vi.fn().mockResolvedValue(undefined),
notifyPermission: vi.fn().mockResolvedValue(undefined),
notifyConnection: vi.fn().mockResolvedValue(undefined),
notifyTaskStart: vi.fn().mockResolvedValue(undefined),
},
}));
describe("testNotifications", () => {
describe("window assignment", () => {
it("assigns testAllNotifications to window.testNotifications", async () => {
// The module-level if block runs on import — reimport to ensure it ran
await import("./testNotifications");
expect((window as unknown as { testNotifications: unknown }).testNotifications).toBe(
testAllNotifications
);
});
});
describe("testAllNotifications", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("is an async function", () => {
expect(typeof testAllNotifications).toBe("function");
});
it("schedules all five notification type calls", async () => {
const { notificationManager } = await import("./notificationManager");
const consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
await testAllNotifications();
await vi.runAllTimersAsync();
expect(notificationManager.notifySuccess).toHaveBeenCalledWith("Test task completed!");
expect(notificationManager.notifyError).toHaveBeenCalledWith("Test error occurred!");
expect(notificationManager.notifyPermission).toHaveBeenCalledWith("Test permission request!");
expect(notificationManager.notifyConnection).toHaveBeenCalledWith(
"Test connection established!"
);
expect(notificationManager.notifyTaskStart).toHaveBeenCalledWith("Test task starting!");
consoleLogSpy.mockRestore();
});
});
});
+139
View File
@@ -0,0 +1,139 @@
import { describe, it, expect, afterEach } from "vitest";
import { get } from "svelte/store";
import {
claudeStore,
hasPermissionPending,
hasQuestionPending,
isClaudeProcessing,
} from "./claude";
import { conversationsStore } from "./conversations";
import { characterState } from "$lib/stores/character";
import type { PermissionRequest, UserQuestionEvent } from "$lib/types/messages";
describe("claudeStore (compatibility wrapper)", () => {
afterEach(() => {
conversationsStore.revokeAllTools();
conversationsStore.clearPermission();
conversationsStore.clearQuestion();
conversationsStore.setConnectionStatus("disconnected");
characterState.setState("idle");
});
describe("getGrantedTools", () => {
it("returns an empty array when no tools are granted", () => {
expect(claudeStore.getGrantedTools()).toEqual([]);
});
it("returns granted tools as an array", () => {
conversationsStore.grantTool("Read");
conversationsStore.grantTool("Write");
const tools = claudeStore.getGrantedTools();
expect(tools).toContain("Read");
expect(tools).toContain("Write");
});
});
describe("reset", () => {
it("clears terminal lines and resets processing state", () => {
conversationsStore.setProcessing(true);
conversationsStore.grantTool("Edit");
claudeStore.reset();
expect(get(conversationsStore.isProcessing)).toBe(false);
expect(claudeStore.getGrantedTools()).toEqual([]);
});
});
});
describe("hasPermissionPending derived store", () => {
afterEach(() => {
conversationsStore.clearPermission();
});
it("is false when there are no pending permissions", () => {
expect(get(hasPermissionPending)).toBe(false);
});
it("is true when there is a pending permission request", () => {
const request: PermissionRequest = {
id: "perm-1",
tool: "Bash",
description: "Run a shell command",
input: { command: "ls" },
};
conversationsStore.requestPermission(request);
expect(get(hasPermissionPending)).toBe(true);
conversationsStore.clearPermission();
});
});
describe("hasQuestionPending derived store", () => {
afterEach(() => {
conversationsStore.clearQuestion();
});
it("is false when there is no pending question", () => {
expect(get(hasQuestionPending)).toBe(false);
});
it("is true when there is a pending question", () => {
const question: UserQuestionEvent = {
id: "q-1",
question: "Which approach do you prefer?",
options: [{ label: "A" }, { label: "B" }],
multi_select: false,
};
conversationsStore.requestQuestion(question);
expect(get(hasQuestionPending)).toBe(true);
});
});
describe("isClaudeProcessing derived store", () => {
afterEach(() => {
conversationsStore.setConnectionStatus("disconnected");
characterState.setState("idle");
});
it("is false when disconnected regardless of character state", () => {
conversationsStore.setConnectionStatus("disconnected");
characterState.setState("thinking");
expect(get(isClaudeProcessing)).toBe(false);
});
it("is false when connected but in idle state", () => {
conversationsStore.setConnectionStatus("connected");
characterState.setState("idle");
expect(get(isClaudeProcessing)).toBe(false);
});
it("is true when connected and in thinking state", () => {
conversationsStore.setConnectionStatus("connected");
characterState.setState("thinking");
expect(get(isClaudeProcessing)).toBe(true);
});
it("is true when connected and in coding state", () => {
conversationsStore.setConnectionStatus("connected");
characterState.setState("coding");
expect(get(isClaudeProcessing)).toBe(true);
});
it("is true when connected and in searching state", () => {
conversationsStore.setConnectionStatus("connected");
characterState.setState("searching");
expect(get(isClaudeProcessing)).toBe(true);
});
});