generated from nhcarrigan/template
test: add coverage for slashCommands execute functions and notificationManager Method 4
This commit is contained in:
@@ -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<typeof vi.fn>;
|
||||
let invokeMock: ReturnType<typeof vi.fn>;
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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!"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user