From 55ad039451a640925c4e187c4bf43701e1022027 Mon Sep 17 00:00:00 2001 From: Hikari Date: Tue, 3 Mar 2026 16:06:35 -0800 Subject: [PATCH] test: add coverage for slashCommands execute functions and notificationManager Method 4 --- src/lib/commands/slashCommands.test.ts | 302 +++++++++++++++++- .../notifications/notificationManager.test.ts | 54 ++++ 2 files changed, 355 insertions(+), 1 deletion(-) diff --git a/src/lib/commands/slashCommands.test.ts b/src/lib/commands/slashCommands.test.ts index 74c7de5..f2e16b9 100644 --- a/src/lib/commands/slashCommands.test.ts +++ b/src/lib/commands/slashCommands.test.ts @@ -1,4 +1,9 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { get } from "svelte/store"; +import { invoke } from "@tauri-apps/api/core"; +import { claudeStore } from "$lib/stores/claude"; +import { searchState } from "$lib/stores/search"; +import { characterState } from "$lib/stores/character"; import { slashCommands, parseSlashCommand, @@ -40,6 +45,28 @@ vi.mock("$lib/stores/character", () => ({ vi.mock("$lib/tauri", () => ({ setSkipNextGreeting: vi.fn(), + updateDiscordRpc: vi.fn().mockResolvedValue(undefined), +})); + +vi.mock("$lib/stores/conversations", () => ({ + conversationsStore: { + activeConversation: { subscribe: vi.fn() }, + }, +})); + +vi.mock("$lib/stores/config", () => ({ + configStore: { + getConfig: vi.fn().mockReturnValue({ + auto_granted_tools: [], + model: "claude-sonnet", + api_key: null, + custom_instructions: null, + mcp_servers_json: null, + use_worktree: false, + disable_1m_context: false, + max_output_tokens: null, + }), + }, })); vi.mock("$lib/stores/search", () => ({ @@ -415,4 +442,277 @@ describe("slashCommands", () => { expect(result).toBeUndefined(); }); }); + + describe("command execute functions", () => { + let getMock: ReturnType; + let invokeMock: ReturnType; + + beforeEach(() => { + vi.clearAllMocks(); + getMock = vi.mocked(get); + invokeMock = vi.mocked(invoke); + }); + + describe("/clear execute", () => { + it("clears terminal and shows confirmation message", () => { + const clearCmd = slashCommands.find((cmd) => cmd.name === "clear")!; + clearCmd.execute(""); + expect(claudeStore.clearTerminal).toHaveBeenCalledWith(); + expect(claudeStore.addLine).toHaveBeenCalledWith("system", "Terminal cleared"); + }); + }); + + describe("/help execute", () => { + it("shows available commands header", () => { + const helpCmd = slashCommands.find((cmd) => cmd.name === "help")!; + helpCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + expect.stringContaining("Available commands:") + ); + }); + + it("includes all command usages in help text", () => { + const helpCmd = slashCommands.find((cmd) => cmd.name === "help")!; + helpCmd.execute(""); + const callArgs = vi.mocked(claudeStore.addLine).mock.calls[0]; + const helpText = callArgs[1] as string; + expect(helpText).toContain("/cd"); + expect(helpText).toContain("/clear"); + expect(helpText).toContain("/help"); + expect(helpText).toContain("/search"); + expect(helpText).toContain("/new"); + expect(helpText).toContain("/summarise"); + expect(helpText).toContain("/skill"); + }); + + it("includes command descriptions in help text", () => { + const helpCmd = slashCommands.find((cmd) => cmd.name === "help")!; + helpCmd.execute(""); + const callArgs = vi.mocked(claudeStore.addLine).mock.calls[0]; + const helpText = callArgs[1] as string; + expect(helpText).toContain("Change the working directory"); + expect(helpText).toContain("Show available slash commands"); + }); + }); + + describe("/search execute", () => { + it("clears search when called with empty args", () => { + const searchCmd = slashCommands.find((cmd) => cmd.name === "search")!; + searchCmd.execute(""); + expect(searchState.clear).toHaveBeenCalledWith(); + expect(claudeStore.addLine).toHaveBeenCalledWith("system", "Search cleared"); + }); + + it("clears search when called with whitespace-only args", () => { + const searchCmd = slashCommands.find((cmd) => cmd.name === "search")!; + searchCmd.execute(" "); + expect(searchState.clear).toHaveBeenCalledWith(); + expect(claudeStore.addLine).toHaveBeenCalledWith("system", "Search cleared"); + }); + + it("sets query when called with a search term", () => { + const searchCmd = slashCommands.find((cmd) => cmd.name === "search")!; + searchCmd.execute("hello world"); + expect(searchState.setQuery).toHaveBeenCalledWith("hello world"); + expect(claudeStore.addLine).toHaveBeenCalledWith("system", 'Searching for: "hello world"'); + }); + + it("trims whitespace from query before setting", () => { + const searchCmd = slashCommands.find((cmd) => cmd.name === "search")!; + searchCmd.execute(" hello "); + expect(searchState.setQuery).toHaveBeenCalledWith("hello"); + expect(claudeStore.addLine).toHaveBeenCalledWith("system", 'Searching for: "hello"'); + }); + }); + + describe("/cd execute", () => { + it("shows error when no active conversation", async () => { + getMock.mockReturnValue(null); + const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; + await cdCmd.execute("/some/path"); + expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); + }); + + it("shows current directory when called with empty args", async () => { + getMock.mockReturnValueOnce("conv-123").mockReturnValueOnce("/home/naomi/code"); + const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; + await cdCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + "Current directory: /home/naomi/code" + ); + }); + + it("shows current directory when called with whitespace-only args", async () => { + getMock.mockReturnValueOnce("conv-123").mockReturnValueOnce("/home/naomi/code"); + const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; + await cdCmd.execute(" "); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + "Current directory: /home/naomi/code" + ); + }); + + it("validates path and changes directory on success", async () => { + getMock.mockReturnValueOnce("conv-123").mockReturnValueOnce(null); + invokeMock.mockResolvedValue("/validated/path"); + const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; + await cdCmd.execute("/new/path"); + expect(invokeMock).toHaveBeenCalledWith( + "validate_directory", + expect.objectContaining({ path: "/new/path" }) + ); + }); + + it("shows error when directory change fails", async () => { + getMock.mockReturnValueOnce("conv-123"); + invokeMock.mockRejectedValueOnce(new Error("invalid path")); + const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; + await cdCmd.execute("/bad/path"); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "error", + expect.stringContaining("Failed to change directory:") + ); + expect(characterState.setTemporaryState).toHaveBeenCalledWith("error", 3000); + }); + }); + + describe("/new execute", () => { + it("shows error when no active conversation", async () => { + getMock.mockReturnValue(null); + const newCmd = slashCommands.find((cmd) => cmd.name === "new")!; + await newCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); + }); + + it("shows error when starting new conversation fails", async () => { + getMock.mockReturnValueOnce("conv-123"); + invokeMock.mockRejectedValueOnce(new Error("invoke failed")); + const newCmd = slashCommands.find((cmd) => cmd.name === "new")!; + await newCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "error", + expect.stringContaining("Failed to start new conversation:") + ); + expect(characterState.setTemporaryState).toHaveBeenCalledWith("error", 3000); + }); + }); + + describe("/summarise execute", () => { + it("shows error when no active conversation", async () => { + getMock.mockReturnValue(null); + const summariseCmd = slashCommands.find((cmd) => cmd.name === "summarise")!; + await summariseCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); + }); + + it("sends a summary prompt when there is an active conversation", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockResolvedValue(undefined); + const summariseCmd = slashCommands.find((cmd) => cmd.name === "summarise")!; + await summariseCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + "Requesting conversation summary..." + ); + expect(invokeMock).toHaveBeenCalledWith( + "send_prompt", + expect.objectContaining({ conversationId: "conv-123" }) + ); + }); + + it("shows error when send_prompt invoke fails", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockRejectedValue(new Error("network error")); + const summariseCmd = slashCommands.find((cmd) => cmd.name === "summarise")!; + await summariseCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "error", + expect.stringContaining("Failed to request summary:") + ); + }); + }); + + describe("/skill execute", () => { + it("shows error when no active conversation", async () => { + getMock.mockReturnValue(null); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute("onboard-mentee"); + expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); + }); + + it("lists available skills when called with no name", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockResolvedValue(["onboard-mentee", "other-skill"]); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute(""); + expect(invokeMock).toHaveBeenCalledWith("list_skills"); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + expect.stringContaining("onboard-mentee") + ); + }); + + it("shows empty message when no skills are found", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockResolvedValue([]); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + expect.stringContaining("No skills found") + ); + }); + + it("invokes skill when called with a name and no data", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockResolvedValue(undefined); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute("onboard-mentee"); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "system", + "Invoking skill: onboard-mentee" + ); + expect(invokeMock).toHaveBeenCalledWith( + "send_prompt", + expect.objectContaining({ conversationId: "conv-123" }) + ); + }); + + it("invokes skill with additional data in the prompt", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockResolvedValue(undefined); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute("onboard-mentee some extra data"); + expect(invokeMock).toHaveBeenCalledWith("send_prompt", { + conversationId: "conv-123", + message: expect.stringContaining("some extra data"), + }); + }); + + it("shows error when listing skills fails", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockRejectedValue(new Error("list error")); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute(""); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "error", + expect.stringContaining("Failed to list skills:") + ); + }); + + it("shows error and resets character state when invoking skill fails", async () => { + getMock.mockReturnValue("conv-123"); + invokeMock.mockRejectedValue(new Error("invoke error")); + const skillCmd = slashCommands.find((cmd) => cmd.name === "skill")!; + await skillCmd.execute("onboard-mentee"); + expect(claudeStore.addLine).toHaveBeenCalledWith( + "error", + expect.stringContaining("Failed to invoke skill:") + ); + expect(characterState.setTemporaryState).toHaveBeenCalledWith("error", 3000); + }); + }); + }); }); diff --git a/src/lib/notifications/notificationManager.test.ts b/src/lib/notifications/notificationManager.test.ts index 187516e..7046235 100644 --- a/src/lib/notifications/notificationManager.test.ts +++ b/src/lib/notifications/notificationManager.test.ts @@ -185,4 +185,58 @@ describe("NotificationManager", () => { }); }); }); + + describe("Tauri plugin notification method (Method 4)", () => { + it("sends notification when permission is already granted", async () => { + const { isPermissionGranted, sendNotification } = + await import("@tauri-apps/plugin-notification"); + setMockInvokeResult("send_windows_notification", new Error("failed")); + setMockInvokeResult("send_windows_toast", new Error("failed")); + setMockInvokeResult("send_wsl_notification", new Error("failed")); + vi.mocked(isPermissionGranted).mockResolvedValueOnce(true); + + await notificationManager.notify(NotificationType.SUCCESS); + + expect(sendNotification).toHaveBeenCalledWith( + expect.objectContaining({ + title: NOTIFICATION_SOUNDS[NotificationType.SUCCESS].phrase, + }) + ); + }); + + it("requests permission and sends notification when not yet granted", async () => { + const { isPermissionGranted, requestPermission, sendNotification } = + await import("@tauri-apps/plugin-notification"); + setMockInvokeResult("send_windows_notification", new Error("failed")); + setMockInvokeResult("send_windows_toast", new Error("failed")); + setMockInvokeResult("send_wsl_notification", new Error("failed")); + vi.mocked(isPermissionGranted).mockResolvedValueOnce(false); + vi.mocked(requestPermission).mockResolvedValueOnce("granted"); + + await notificationManager.notify(NotificationType.SUCCESS); + + expect(requestPermission).toHaveBeenCalledWith(); + expect(sendNotification).toHaveBeenCalledWith( + expect.objectContaining({ body: "Task completed successfully!" }) + ); + }); + + it("falls through to next method when permission is denied", async () => { + const { isPermissionGranted, requestPermission } = + await import("@tauri-apps/plugin-notification"); + setMockInvokeResult("send_windows_notification", new Error("failed")); + setMockInvokeResult("send_windows_toast", new Error("failed")); + setMockInvokeResult("send_wsl_notification", new Error("failed")); + vi.mocked(isPermissionGranted).mockResolvedValueOnce(false); + vi.mocked(requestPermission).mockResolvedValueOnce("denied"); + setMockInvokeResult("send_notify_send", new Error("failed")); + + await notificationManager.notify(NotificationType.SUCCESS); + + expect(sendTerminalNotification).toHaveBeenCalledWith( + NotificationType.SUCCESS, + "Task completed successfully!" + ); + }); + }); });