generated from nhcarrigan/template
b3d79a82ef
### Explanation _No response_ ### Issue _No response_ ### Attestations - [ ] I have read and agree to the [Code of Conduct](https://docs.nhcarrigan.com/community/coc/) - [ ] I have read and agree to the [Community Guidelines](https://docs.nhcarrigan.com/community/guide/). - [ ] My contribution complies with the [Contributor Covenant](https://docs.nhcarrigan.com/dev/covenant/). ### Dependencies - [ ] I have pinned the dependencies to a specific patch version. ### Style - [ ] I have run the linter and resolved any errors. - [ ] My pull request uses an appropriate title, matching the conventional commit standards. - [ ] My scope of feat/fix/chore/etc. correctly matches the nature of changes in my pull request. ### Tests - [ ] My contribution adds new code, and I have added tests to cover it. - [ ] My contribution modifies existing code, and I have updated the tests to reflect these changes. - [ ] All new and existing tests pass locally with my changes. - [ ] Code coverage remains at or above the configured threshold. ### Documentation _No response_ ### Versioning _No response_ Co-authored-by: Hikari <hikari@nhcarrigan.com> Reviewed-on: #71 Co-authored-by: Naomi Carrigan <commits@nhcarrigan.com> Co-committed-by: Naomi Carrigan <commits@nhcarrigan.com>
415 lines
13 KiB
TypeScript
415 lines
13 KiB
TypeScript
import { describe, it, expect, vi } from "vitest";
|
|
import {
|
|
slashCommands,
|
|
parseSlashCommand,
|
|
getMatchingCommands,
|
|
isSlashCommand,
|
|
type SlashCommand,
|
|
} from "./slashCommands";
|
|
|
|
// Mock all external dependencies
|
|
vi.mock("svelte/store", () => ({
|
|
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(),
|
|
}));
|
|
|
|
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");
|
|
});
|
|
|
|
it("has 7 commands total", () => {
|
|
expect(slashCommands.length).toBe(7);
|
|
});
|
|
|
|
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 <path>");
|
|
});
|
|
|
|
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]");
|
|
});
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
});
|