From a5fb22bd237ea5c2bd793e7f2cd724c16f3f6a6d Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 3 Mar 2026 15:17:29 -0800 Subject: [PATCH] test: add coverage for terminalNotifier, testNotifications, and claude store --- .../notifications/terminalNotifier.test.ts | 64 ++++++++ .../notifications/testNotifications.test.ts | 58 ++++++++ src/lib/stores/claude.test.ts | 139 ++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/lib/notifications/terminalNotifier.test.ts create mode 100644 src/lib/notifications/testNotifications.test.ts create mode 100644 src/lib/stores/claude.test.ts diff --git a/src/lib/notifications/terminalNotifier.test.ts b/src/lib/notifications/terminalNotifier.test.ts new file mode 100644 index 0000000..24a177e --- /dev/null +++ b/src/lib/notifications/terminalNotifier.test.ts @@ -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!") + ); + }); +}); diff --git a/src/lib/notifications/testNotifications.test.ts b/src/lib/notifications/testNotifications.test.ts new file mode 100644 index 0000000..f989df3 --- /dev/null +++ b/src/lib/notifications/testNotifications.test.ts @@ -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(); + }); + }); +}); diff --git a/src/lib/stores/claude.test.ts b/src/lib/stores/claude.test.ts new file mode 100644 index 0000000..c8ece4e --- /dev/null +++ b/src/lib/stores/claude.test.ts @@ -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); + }); +});