import { describe, it, expect, vi, beforeEach, afterEach } 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, getMatchingCommands, isSlashCommand, type SlashCommand, } from "./slashCommands"; // Mock all external dependencies vi.mock("svelte/store", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, get: vi.fn(), }; }); vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(), })); vi.mock("$lib/stores/claude", () => ({ claudeStore: { addLine: vi.fn(), clearTerminal: vi.fn(), activeConversationId: { subscribe: vi.fn() }, currentWorkingDirectory: { subscribe: vi.fn() }, setWorkingDirectory: vi.fn(), getConversationHistory: vi.fn(), }, })); vi.mock("$lib/stores/character", () => ({ characterState: { setState: vi.fn(), setTemporaryState: vi.fn(), }, })); 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, include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: null, model_overrides: null, }), }, })); vi.mock("$lib/stores/search", () => ({ searchState: { setQuery: vi.fn(), clear: vi.fn(), }, })); describe("slashCommands", () => { describe("slashCommands array", () => { it("contains expected commands", () => { const commandNames = slashCommands.map((cmd) => cmd.name); expect(commandNames).toContain("cd"); expect(commandNames).toContain("clear"); expect(commandNames).toContain("new"); expect(commandNames).toContain("help"); expect(commandNames).toContain("search"); expect(commandNames).toContain("summarise"); expect(commandNames).toContain("skill"); expect(commandNames).toContain("simplify"); expect(commandNames).toContain("loop"); expect(commandNames).toContain("batch"); expect(commandNames).toContain("memory"); expect(commandNames).toContain("context"); }); it("has 12 commands total", () => { expect(slashCommands.length).toBe(12); }); it("each command has required properties", () => { slashCommands.forEach((cmd) => { expect(cmd.name).toBeDefined(); expect(typeof cmd.name).toBe("string"); expect(cmd.name.length).toBeGreaterThan(0); expect(cmd.description).toBeDefined(); expect(typeof cmd.description).toBe("string"); expect(cmd.description.length).toBeGreaterThan(0); expect(cmd.usage).toBeDefined(); expect(typeof cmd.usage).toBe("string"); expect(cmd.usage.startsWith("/")).toBe(true); expect(cmd.execute).toBeDefined(); expect(typeof cmd.execute).toBe("function"); }); }); it("cd command has correct metadata", () => { const cdCmd = slashCommands.find((cmd) => cmd.name === "cd"); expect(cdCmd).toBeDefined(); expect(cdCmd!.description).toBe("Change the working directory"); expect(cdCmd!.usage).toBe("/cd "); }); it("clear command has correct metadata", () => { const clearCmd = slashCommands.find((cmd) => cmd.name === "clear"); expect(clearCmd).toBeDefined(); expect(clearCmd!.description).toBe("Clear the terminal display (keeps conversation context)"); expect(clearCmd!.usage).toBe("/clear"); }); it("new command has correct metadata", () => { const newCmd = slashCommands.find((cmd) => cmd.name === "new"); expect(newCmd).toBeDefined(); expect(newCmd!.description).toBe("Start a fresh conversation (resets context)"); expect(newCmd!.usage).toBe("/new"); }); it("help command has correct metadata", () => { const helpCmd = slashCommands.find((cmd) => cmd.name === "help"); expect(helpCmd).toBeDefined(); expect(helpCmd!.description).toBe("Show available slash commands"); expect(helpCmd!.usage).toBe("/help"); }); it("search command has correct metadata", () => { const searchCmd = slashCommands.find((cmd) => cmd.name === "search"); expect(searchCmd).toBeDefined(); expect(searchCmd!.description).toBe("Search within the conversation (use /search to clear)"); expect(searchCmd!.usage).toBe("/search [query]"); }); it("summarise command has correct metadata", () => { const summariseCmd = slashCommands.find((cmd) => cmd.name === "summarise"); expect(summariseCmd).toBeDefined(); expect(summariseCmd!.description).toBe("Get a summary of the entire conversation"); expect(summariseCmd!.usage).toBe("/summarise"); }); it("skill command has correct metadata", () => { const skillCmd = slashCommands.find((cmd) => cmd.name === "skill"); expect(skillCmd).toBeDefined(); expect(skillCmd!.description).toBe("Invoke a Claude Code skill from ~/.claude/skills/"); expect(skillCmd!.usage).toBe("/skill [name] [data]"); }); it("simplify command has correct metadata and source", () => { const simplifyCmd = slashCommands.find((cmd) => cmd.name === "simplify"); expect(simplifyCmd).toBeDefined(); expect(simplifyCmd!.source).toBe("cli"); expect(simplifyCmd!.usage).toBe("/simplify"); }); it("loop command has correct metadata and source", () => { const loopCmd = slashCommands.find((cmd) => cmd.name === "loop"); expect(loopCmd).toBeDefined(); expect(loopCmd!.source).toBe("cli"); expect(loopCmd!.usage).toBe("/loop [interval] [command]"); }); it("batch command has correct metadata and source", () => { const batchCmd = slashCommands.find((cmd) => cmd.name === "batch"); expect(batchCmd).toBeDefined(); expect(batchCmd!.source).toBe("cli"); expect(batchCmd!.usage).toBe("/batch [tasks]"); }); it("context command has correct metadata and source", () => { const contextCmd = slashCommands.find((cmd) => cmd.name === "context"); expect(contextCmd).toBeDefined(); expect(contextCmd!.source).toBe("cli"); expect(contextCmd!.usage).toBe("/context"); }); it("app commands do not have source set", () => { const appCommandNames = ["cd", "clear", "new", "help", "search", "summarise", "skill"]; appCommandNames.forEach((name) => { const cmd = slashCommands.find((c) => c.name === name); expect(cmd).toBeDefined(); expect(cmd!.source).toBeUndefined(); }); }); it("cli commands have source set to 'cli'", () => { const cliCommandNames = ["simplify", "loop", "batch", "memory", "context"]; cliCommandNames.forEach((name) => { const cmd = slashCommands.find((c) => c.name === name); expect(cmd).toBeDefined(); expect(cmd!.source).toBe("cli"); }); }); }); describe("parseSlashCommand", () => { it("returns null for non-slash input", () => { const result = parseSlashCommand("hello world"); expect(result.command).toBeNull(); expect(result.args).toBe(""); }); it("returns null for empty string", () => { const result = parseSlashCommand(""); expect(result.command).toBeNull(); expect(result.args).toBe(""); }); it("returns null for whitespace only", () => { const result = parseSlashCommand(" "); expect(result.command).toBeNull(); expect(result.args).toBe(""); }); it("parses /cd command without args", () => { const result = parseSlashCommand("/cd"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("cd"); expect(result.args).toBe(""); }); it("parses /cd command with path argument", () => { const result = parseSlashCommand("/cd /home/naomi/code"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("cd"); expect(result.args).toBe("/home/naomi/code"); }); it("parses /clear command", () => { const result = parseSlashCommand("/clear"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("clear"); expect(result.args).toBe(""); }); it("parses /new command", () => { const result = parseSlashCommand("/new"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("new"); expect(result.args).toBe(""); }); it("parses /help command", () => { const result = parseSlashCommand("/help"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("help"); expect(result.args).toBe(""); }); it("parses /search command with query", () => { const result = parseSlashCommand("/search hello world"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("search"); expect(result.args).toBe("hello world"); }); it("parses /search command without query", () => { const result = parseSlashCommand("/search"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("search"); expect(result.args).toBe(""); }); it("parses /summarise command", () => { const result = parseSlashCommand("/summarise"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("summarise"); expect(result.args).toBe(""); }); it("parses /skill command with name and data", () => { const result = parseSlashCommand("/skill onboard-mentee john@example.com"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("skill"); expect(result.args).toBe("onboard-mentee john@example.com"); }); it("parses /skill command with name only", () => { const result = parseSlashCommand("/skill onboard-mentee"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("skill"); expect(result.args).toBe("onboard-mentee"); }); it("parses /skill command without arguments", () => { const result = parseSlashCommand("/skill"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("skill"); expect(result.args).toBe(""); }); it("returns null for unknown command", () => { const result = parseSlashCommand("/unknown"); expect(result.command).toBeNull(); expect(result.args).toBe(""); }); it("is case insensitive for command names", () => { const result1 = parseSlashCommand("/CD /path"); expect(result1.command).not.toBeNull(); expect(result1.command!.name).toBe("cd"); const result2 = parseSlashCommand("/CLEAR"); expect(result2.command).not.toBeNull(); expect(result2.command!.name).toBe("clear"); const result3 = parseSlashCommand("/Help"); expect(result3.command).not.toBeNull(); expect(result3.command!.name).toBe("help"); }); it("handles leading whitespace", () => { const result = parseSlashCommand(" /cd /path"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("cd"); expect(result.args).toBe("/path"); }); it("handles trailing whitespace", () => { const result = parseSlashCommand("/cd /path "); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("cd"); expect(result.args).toBe("/path"); }); it("handles multiple spaces between args", () => { const result = parseSlashCommand("/search hello world"); expect(result.command).not.toBeNull(); expect(result.command!.name).toBe("search"); expect(result.args).toBe("hello world"); }); }); describe("getMatchingCommands", () => { it("returns empty array for non-slash input", () => { const result = getMatchingCommands("hello"); expect(result).toEqual([]); }); it("returns empty array for empty string", () => { const result = getMatchingCommands(""); expect(result).toEqual([]); }); it("returns all commands for just slash", () => { const result = getMatchingCommands("/"); expect(result.length).toBe(slashCommands.length); }); it("returns matching commands for partial input", () => { const result = getMatchingCommands("/c"); const names = result.map((cmd) => cmd.name); expect(names).toContain("cd"); expect(names).toContain("clear"); expect(names).not.toContain("help"); }); it("returns single command for exact match", () => { const result = getMatchingCommands("/cd"); expect(result.length).toBe(1); expect(result[0].name).toBe("cd"); }); it("returns single command for partial unique match", () => { const result = getMatchingCommands("/cl"); expect(result.length).toBe(1); expect(result[0].name).toBe("clear"); }); it("returns matching commands for /s prefix", () => { const result = getMatchingCommands("/s"); const names = result.map((cmd) => cmd.name); expect(names).toContain("search"); expect(names).toContain("summarise"); expect(names).toContain("skill"); expect(names).toContain("simplify"); }); it("returns /loop for /l prefix", () => { const result = getMatchingCommands("/l"); const names = result.map((cmd) => cmd.name); expect(names).toContain("loop"); }); it("returns /batch for /b prefix", () => { const result = getMatchingCommands("/b"); const names = result.map((cmd) => cmd.name); expect(names).toContain("batch"); }); it("is case insensitive", () => { const result1 = getMatchingCommands("/C"); const result2 = getMatchingCommands("/c"); expect(result1.length).toBe(result2.length); }); it("returns empty array for no matches", () => { const result = getMatchingCommands("/xyz"); expect(result).toEqual([]); }); it("handles whitespace correctly", () => { const result = getMatchingCommands(" /c"); const names = result.map((cmd) => cmd.name); expect(names).toContain("cd"); expect(names).toContain("clear"); }); it("returns command for full command name", () => { const result = getMatchingCommands("/help"); expect(result.length).toBe(1); expect(result[0].name).toBe("help"); }); it("returns command for /new", () => { const result = getMatchingCommands("/n"); expect(result.length).toBe(1); expect(result[0].name).toBe("new"); }); }); describe("isSlashCommand", () => { it("returns true for input starting with slash", () => { expect(isSlashCommand("/cd")).toBe(true); expect(isSlashCommand("/")).toBe(true); expect(isSlashCommand("/help")).toBe(true); expect(isSlashCommand("/unknown")).toBe(true); }); it("returns false for non-slash input", () => { expect(isSlashCommand("hello")).toBe(false); expect(isSlashCommand("")).toBe(false); expect(isSlashCommand("cd")).toBe(false); }); it("handles whitespace correctly", () => { expect(isSlashCommand(" /cd")).toBe(true); expect(isSlashCommand(" hello")).toBe(false); }); it("returns false for slash in middle of string", () => { expect(isSlashCommand("hello/world")).toBe(false); }); }); describe("SlashCommand interface", () => { it("can create a valid slash command object", () => { const testCommand: SlashCommand = { name: "test", description: "A test command", usage: "/test [arg]", execute: vi.fn(), }; expect(testCommand.name).toBe("test"); expect(testCommand.description).toBe("A test command"); expect(testCommand.usage).toBe("/test [arg]"); expect(typeof testCommand.execute).toBe("function"); expect(testCommand.source).toBeUndefined(); }); it("can create a cli-sourced slash command object", () => { const cliCommand: SlashCommand = { name: "cli-test", description: "A CLI command", usage: "/cli-test", source: "cli", execute: vi.fn(), }; expect(cliCommand.source).toBe("cli"); }); it("execute can be async function", () => { const asyncCommand: SlashCommand = { name: "async", description: "An async command", usage: "/async", execute: async () => { await Promise.resolve(); }, }; expect(asyncCommand.execute("")).toBeInstanceOf(Promise); }); it("execute can be sync function", () => { const syncCommand: SlashCommand = { name: "sync", description: "A sync command", usage: "/sync", execute: () => { // Synchronous execution }, }; const result = syncCommand.execute(""); // Sync function returns undefined, not a Promise 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); }); }); describe("/simplify execute", () => { it("shows error when no active conversation", async () => { getMock.mockReturnValue(null); const simplifyCmd = slashCommands.find((cmd) => cmd.name === "simplify")!; await simplifyCmd.execute(""); expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); }); it("sends /simplify prompt to Claude when there is an active conversation", async () => { getMock.mockReturnValue("conv-123"); invokeMock.mockResolvedValue(undefined); const simplifyCmd = slashCommands.find((cmd) => cmd.name === "simplify")!; await simplifyCmd.execute(""); expect(invokeMock).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: "/simplify", }); }); }); describe("/loop execute", () => { it("shows error when no active conversation", async () => { getMock.mockReturnValue(null); const loopCmd = slashCommands.find((cmd) => cmd.name === "loop")!; await loopCmd.execute("5m /help"); expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); }); it("sends /loop with args when args are provided", async () => { getMock.mockReturnValue("conv-123"); invokeMock.mockResolvedValue(undefined); const loopCmd = slashCommands.find((cmd) => cmd.name === "loop")!; await loopCmd.execute("5m /help"); expect(invokeMock).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: "/loop 5m /help", }); }); it("sends /loop without args when no args provided", async () => { getMock.mockReturnValue("conv-123"); invokeMock.mockResolvedValue(undefined); const loopCmd = slashCommands.find((cmd) => cmd.name === "loop")!; await loopCmd.execute(""); expect(invokeMock).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: "/loop", }); }); }); describe("/batch execute", () => { it("shows error when no active conversation", async () => { getMock.mockReturnValue(null); const batchCmd = slashCommands.find((cmd) => cmd.name === "batch")!; await batchCmd.execute("task1, task2"); expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); }); it("sends /batch with args when args are provided", async () => { getMock.mockReturnValue("conv-123"); invokeMock.mockResolvedValue(undefined); const batchCmd = slashCommands.find((cmd) => cmd.name === "batch")!; await batchCmd.execute("task1, task2"); expect(invokeMock).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: "/batch task1, task2", }); }); it("sends /batch without args when no args provided", async () => { getMock.mockReturnValue("conv-123"); invokeMock.mockResolvedValue(undefined); const batchCmd = slashCommands.find((cmd) => cmd.name === "batch")!; await batchCmd.execute(""); expect(invokeMock).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: "/batch", }); }); }); describe("/context execute", () => { it("shows error when no active conversation", async () => { getMock.mockReturnValue(null); const contextCmd = slashCommands.find((cmd) => cmd.name === "context")!; await contextCmd.execute(""); expect(claudeStore.addLine).toHaveBeenCalledWith("error", "No active conversation"); }); it("sends /context prompt to Claude when there is an active conversation", async () => { getMock.mockReturnValue("conv-123"); invokeMock.mockResolvedValue(undefined); const contextCmd = slashCommands.find((cmd) => cmd.name === "context")!; await contextCmd.execute(""); expect(invokeMock).toHaveBeenCalledWith("send_prompt", { conversationId: "conv-123", message: "/context", }); }); }); describe("/cd success path", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("changes directory and shows success message", async () => { getMock .mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId) .mockReturnValueOnce("/current") // get(claudeStore.currentWorkingDirectory) .mockReturnValueOnce(null); // get(conversationsStore.activeConversation) vi.mocked(claudeStore.getConversationHistory).mockReturnValue(""); invokeMock .mockResolvedValueOnce("/new/path") // validate_directory .mockResolvedValueOnce(undefined) // stop_claude .mockResolvedValueOnce(undefined); // start_claude const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; const promise = cdCmd.execute("/new/path"); await vi.runAllTimersAsync(); await promise; expect(claudeStore.addLine).toHaveBeenCalledWith( "system", "Changed directory to: /new/path" ); expect(characterState.setState).toHaveBeenCalledWith("idle"); }); it("sends context restoration message when conversation history exists", async () => { getMock .mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId) .mockReturnValueOnce("/current") // get(claudeStore.currentWorkingDirectory) .mockReturnValueOnce(null); // get(conversationsStore.activeConversation) vi.mocked(claudeStore.getConversationHistory).mockReturnValue( "previous conversation history" ); invokeMock .mockResolvedValueOnce("/new/path") // validate_directory .mockResolvedValueOnce(undefined) // stop_claude .mockResolvedValueOnce(undefined) // start_claude .mockResolvedValueOnce(undefined); // send_prompt const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; const promise = cdCmd.execute("/new/path"); await vi.runAllTimersAsync(); await promise; expect(invokeMock).toHaveBeenCalledWith( "send_prompt", expect.objectContaining({ message: expect.stringContaining("previous conversation history"), }) ); expect(claudeStore.addLine).toHaveBeenCalledWith( "system", "Changed directory to: /new/path" ); }); it("calls updateDiscordRpc when activeConversation is available", async () => { const activeConv = { name: "Test Conversation", model: "claude-sonnet", startedAt: new Date("2026-03-03T12:00:00Z"), grantedTools: new Set(), }; getMock .mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId) .mockReturnValueOnce("/current") // get(claudeStore.currentWorkingDirectory) .mockReturnValueOnce(activeConv); // get(conversationsStore.activeConversation) vi.mocked(claudeStore.getConversationHistory).mockReturnValue(""); invokeMock .mockResolvedValueOnce("/new/path") .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined); const { updateDiscordRpc } = await import("$lib/tauri"); const cdCmd = slashCommands.find((cmd) => cmd.name === "cd")!; const promise = cdCmd.execute("/new/path"); await vi.runAllTimersAsync(); await promise; expect(updateDiscordRpc).toHaveBeenCalledWith( "Test Conversation", expect.any(String), expect.any(Date) ); }); }); describe("/new success path", () => { beforeEach(() => { vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); it("starts a new conversation and shows success message", async () => { getMock .mockReturnValueOnce("conv-123") // get(claudeStore.activeConversationId) .mockReturnValueOnce(null); // get(conversationsStore.activeConversation) invokeMock .mockResolvedValueOnce("/working/dir") // get_working_directory .mockResolvedValueOnce(undefined) // interrupt_claude .mockResolvedValueOnce(undefined); // start_claude const newCmd = slashCommands.find((cmd) => cmd.name === "new")!; const promise = newCmd.execute(""); await vi.runAllTimersAsync(); await promise; expect(claudeStore.addLine).toHaveBeenCalledWith("system", "New conversation started!"); expect(characterState.setState).toHaveBeenCalledWith("idle"); }); it("calls updateDiscordRpc when activeConversation is available", async () => { const activeConv = { name: "My Conv", model: "claude-sonnet", startedAt: new Date("2026-03-03T12:00:00Z"), grantedTools: new Set(["tool1"]), }; getMock.mockReturnValueOnce("conv-123").mockReturnValueOnce(activeConv); invokeMock .mockResolvedValueOnce("/working/dir") .mockResolvedValueOnce(undefined) .mockResolvedValueOnce(undefined); const { updateDiscordRpc } = await import("$lib/tauri"); const newCmd = slashCommands.find((cmd) => cmd.name === "new")!; const promise = newCmd.execute(""); await vi.runAllTimersAsync(); await promise; expect(updateDiscordRpc).toHaveBeenCalledWith( "My Conv", expect.any(String), expect.any(Date) ); }); }); }); });