generated from nhcarrigan/template
Compare commits
6 Commits
7e45c685d3
...
55ad039451
| Author | SHA1 | Date | |
|---|---|---|---|
|
55ad039451
|
|||
| ea53bf0d4d | |||
| a5fb22bd23 | |||
| c819adc9ea | |||
| 58f53a421b | |||
| acebe590c3 |
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { isPermissionGranted } from "@tauri-apps/plugin-notification";
|
||||
import { setMockInvokeResult } from "../../../vitest.setup";
|
||||
import { notificationManager } from "./notificationManager";
|
||||
import { sendTerminalNotification } from "./terminalNotifier";
|
||||
import { NotificationType, NOTIFICATION_SOUNDS } from "./types";
|
||||
|
||||
vi.mock("./soundPlayer", () => ({
|
||||
soundPlayer: { play: vi.fn().mockResolvedValue(undefined) },
|
||||
}));
|
||||
|
||||
vi.mock("./terminalNotifier", () => ({
|
||||
sendTerminalNotification: vi.fn(),
|
||||
}));
|
||||
|
||||
describe("NotificationManager", () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleWarnSpy: ReturnType<typeof vi.spyOn>;
|
||||
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe("notify()", () => {
|
||||
it("calls the first notification method by default", async () => {
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
"send_windows_notification",
|
||||
expect.objectContaining({
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.SUCCESS].phrase,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("passes a custom message to the notification", async () => {
|
||||
await notificationManager.notify(NotificationType.ERROR, "Custom error message");
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.ERROR].phrase,
|
||||
body: "Custom error message",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the next method when the first fails", async () => {
|
||||
setMockInvokeResult("send_windows_notification", new Error("Method 1 failed"));
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_toast", expect.any(Object));
|
||||
});
|
||||
|
||||
it("falls back to terminal notification when all methods fail", async () => {
|
||||
setMockInvokeResult("send_windows_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_windows_toast", new Error("failed"));
|
||||
setMockInvokeResult("send_wsl_notification", new Error("failed"));
|
||||
setMockInvokeResult("send_notify_send", new Error("failed"));
|
||||
vi.mocked(isPermissionGranted).mockRejectedValueOnce(new Error("Permission check failed"));
|
||||
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
|
||||
expect(sendTerminalNotification).toHaveBeenCalledWith(
|
||||
NotificationType.SUCCESS,
|
||||
"Task completed successfully!"
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the default SUCCESS message when none is provided", async () => {
|
||||
await notificationManager.notify(NotificationType.SUCCESS);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Task completed successfully!",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default ERROR message", async () => {
|
||||
await notificationManager.notify(NotificationType.ERROR);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Something went wrong...",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default PERMISSION message", async () => {
|
||||
await notificationManager.notify(NotificationType.PERMISSION);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Permission needed to continue",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default CONNECTION message", async () => {
|
||||
await notificationManager.notify(NotificationType.CONNECTION);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Successfully connected to Claude Code",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default TASK_START message", async () => {
|
||||
await notificationManager.notify(NotificationType.TASK_START);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Starting task...",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the default COST_ALERT message", async () => {
|
||||
await notificationManager.notify(NotificationType.COST_ALERT);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "You've exceeded your cost threshold!",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the fallback default message for unhandled types", async () => {
|
||||
await notificationManager.notify(NotificationType.ACHIEVEMENT);
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "Notification",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("helper methods", () => {
|
||||
it("notifySuccess calls notify with SUCCESS type and default message", async () => {
|
||||
await notificationManager.notifySuccess();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.SUCCESS].phrase,
|
||||
body: "Task completed successfully!",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifySuccess passes a custom message", async () => {
|
||||
await notificationManager.notifySuccess("All done!");
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: expect.any(String),
|
||||
body: "All done!",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyError calls notify with ERROR type", async () => {
|
||||
await notificationManager.notifyError();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.ERROR].phrase,
|
||||
body: "Something went wrong...",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyPermission calls notify with PERMISSION type", async () => {
|
||||
await notificationManager.notifyPermission();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.PERMISSION].phrase,
|
||||
body: "Permission needed to continue",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyConnection calls notify with CONNECTION type", async () => {
|
||||
await notificationManager.notifyConnection();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.CONNECTION].phrase,
|
||||
body: "Successfully connected to Claude Code",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyTaskStart calls notify with TASK_START type", async () => {
|
||||
await notificationManager.notifyTaskStart();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.TASK_START].phrase,
|
||||
body: "Starting task...",
|
||||
});
|
||||
});
|
||||
|
||||
it("notifyCostAlert calls notify with COST_ALERT type", async () => {
|
||||
await notificationManager.notifyCostAlert();
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: NOTIFICATION_SOUNDS[NotificationType.COST_ALERT].phrase,
|
||||
body: "You've exceeded your cost threshold!",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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!"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -232,6 +232,53 @@ describe("notifications", () => {
|
||||
// Should not throw
|
||||
await expect(soundPlayer.play(NotificationType.SUCCESS)).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it("play warns when audio type is not in the cache", async () => {
|
||||
const { soundPlayer } = await import("./soundPlayer");
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
|
||||
soundPlayer.setEnabled(true);
|
||||
await soundPlayer.play("nonexistent" as NotificationType);
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith("No audio found for notification type: nonexistent");
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("play catches errors from audio playback", async () => {
|
||||
vi.resetModules();
|
||||
|
||||
class FailingAudio {
|
||||
volume = 1;
|
||||
preload = "auto";
|
||||
|
||||
cloneNode() {
|
||||
const clone = new FailingAudio();
|
||||
clone.volume = this.volume;
|
||||
return clone;
|
||||
}
|
||||
|
||||
async play(): Promise<void> {
|
||||
throw new Error("Playback blocked by browser");
|
||||
}
|
||||
}
|
||||
|
||||
const originalAudio = globalThis.Audio;
|
||||
globalThis.Audio = FailingAudio as unknown as typeof Audio;
|
||||
|
||||
const { soundPlayer: freshPlayer } = await import("./soundPlayer");
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
freshPlayer.setEnabled(true);
|
||||
await freshPlayer.play(NotificationType.SUCCESS);
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Failed to play notification sound:",
|
||||
expect.any(Error)
|
||||
);
|
||||
|
||||
errorSpy.mockRestore();
|
||||
globalThis.Audio = originalAudio;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationManager class", () => {
|
||||
|
||||
@@ -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!")
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { sendWSLNotification } from "./wslNotificationHelper";
|
||||
|
||||
// platform() is mocked in vitest.setup.ts to return "linux" by default
|
||||
|
||||
describe("sendWSLNotification", () => {
|
||||
it("invokes send_windows_notification when platform is windows", async () => {
|
||||
vi.mocked(platform).mockResolvedValueOnce("windows" as Awaited<ReturnType<typeof platform>>);
|
||||
|
||||
await sendWSLNotification("Test Title", "Test body");
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("send_windows_notification", {
|
||||
title: "Test Title",
|
||||
body: "Test body",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not invoke on non-Windows platforms", async () => {
|
||||
// Default mock returns "linux"
|
||||
await sendWSLNotification("Test Title", "Test body");
|
||||
|
||||
expect(invoke).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("handles invoke errors gracefully and logs them", async () => {
|
||||
vi.mocked(platform).mockResolvedValueOnce("windows" as Awaited<ReturnType<typeof platform>>);
|
||||
vi.mocked(invoke).mockRejectedValueOnce(new Error("Windows notification failed"));
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await sendWSLNotification("Title", "Body");
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith(
|
||||
"Failed to send Windows notification:",
|
||||
expect.any(Error)
|
||||
);
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,185 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { setMockInvokeResult, emitMockEvent } from "../../../vitest.setup";
|
||||
import {
|
||||
achievementsStore,
|
||||
unlockedAchievements,
|
||||
lockedAchievements,
|
||||
achievementsByRarity,
|
||||
achievementProgress,
|
||||
initAchievementsListener,
|
||||
} from "./achievements";
|
||||
import type { AchievementUnlockedEvent } from "$lib/types/achievements";
|
||||
import { playAchievementSound } from "$lib/sounds/achievement";
|
||||
|
||||
vi.mock("$lib/sounds/achievement", () => ({
|
||||
playAchievementSound: vi.fn(),
|
||||
}));
|
||||
|
||||
// Helper to build a minimal unlock event
|
||||
function makeEvent(id: AchievementUnlockedEvent["achievement"]["id"]): AchievementUnlockedEvent {
|
||||
return {
|
||||
achievement: {
|
||||
id,
|
||||
name: "Test",
|
||||
description: "Test achievement",
|
||||
icon: "🏆",
|
||||
unlocked_at: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
describe("achievementsStore initial state", () => {
|
||||
it("all achievements start as locked", () => {
|
||||
const state = get(achievementsStore);
|
||||
expect(state.achievements["FirstSteps"].unlocked).toBe(false);
|
||||
expect(state.achievements["GrowingStrong"].unlocked).toBe(false);
|
||||
});
|
||||
|
||||
it("totalUnlocked starts at 0", () => {
|
||||
expect(get(achievementsStore).totalUnlocked).toBe(0);
|
||||
});
|
||||
|
||||
it("lastUnlocked starts as null", () => {
|
||||
expect(get(achievementsStore).lastUnlocked).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived stores initial state", () => {
|
||||
it("unlockedAchievements is initially empty", () => {
|
||||
expect(get(unlockedAchievements)).toEqual([]);
|
||||
});
|
||||
|
||||
it("lockedAchievements contains all achievements initially", () => {
|
||||
const locked = get(lockedAchievements);
|
||||
const total = Object.keys(get(achievementsStore).achievements).length;
|
||||
expect(locked.length).toBe(total);
|
||||
});
|
||||
|
||||
it("achievementsByRarity groups achievements into rarity buckets", () => {
|
||||
const byRarity = get(achievementsByRarity);
|
||||
expect(byRarity.common).toBeInstanceOf(Array);
|
||||
expect(byRarity.rare).toBeInstanceOf(Array);
|
||||
expect(byRarity.epic).toBeInstanceOf(Array);
|
||||
expect(byRarity.legendary).toBeInstanceOf(Array);
|
||||
expect(byRarity.common.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("achievementProgress shows zero unlocked initially", () => {
|
||||
const progress = get(achievementProgress);
|
||||
expect(progress.unlocked).toBe(0);
|
||||
expect(progress.total).toBeGreaterThan(0);
|
||||
expect(progress.percentage).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("achievementsStore.unlockAchievement", () => {
|
||||
it("marks the achievement as unlocked and updates totalUnlocked", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("GrowingStrong"));
|
||||
const state = get(achievementsStore);
|
||||
expect(state.achievements["GrowingStrong"].unlocked).toBe(true);
|
||||
expect(state.totalUnlocked).toBe(1);
|
||||
expect(state.lastUnlocked?.id).toBe("GrowingStrong");
|
||||
});
|
||||
|
||||
it("sets unlockedAt from the event's unlocked_at timestamp", () => {
|
||||
achievementsStore.unlockAchievement({
|
||||
achievement: {
|
||||
id: "BlossomingCoder",
|
||||
name: "Blossoming Coder",
|
||||
description: "100k tokens",
|
||||
icon: "🌸",
|
||||
unlocked_at: "2026-01-15T12:00:00.000Z",
|
||||
},
|
||||
});
|
||||
const state = get(achievementsStore);
|
||||
expect(state.achievements["BlossomingCoder"].unlockedAt).toBeInstanceOf(Date);
|
||||
});
|
||||
|
||||
it("does nothing when the achievement is already unlocked", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenMaster"));
|
||||
const firstTotal = get(achievementsStore).totalUnlocked;
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenMaster"));
|
||||
expect(get(achievementsStore).totalUnlocked).toBe(firstTotal);
|
||||
});
|
||||
|
||||
it("calls playAchievementSound when playSound is true (default)", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenBillionaire"));
|
||||
expect(playAchievementSound).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call playAchievementSound when playSound is false", () => {
|
||||
achievementsStore.unlockAchievement(makeEvent("TokenTreasure"), false);
|
||||
expect(playAchievementSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs an error when playAchievementSound throws", () => {
|
||||
vi.mocked(playAchievementSound).mockImplementationOnce(() => {
|
||||
throw new Error("Sound failed");
|
||||
});
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
achievementsStore.unlockAchievement(makeEvent("HelloWorld"));
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Failed to play achievement sound:", expect.any(Error));
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived stores after unlocks", () => {
|
||||
it("unlockedAchievements includes previously unlocked achievements", () => {
|
||||
const unlocked = get(unlockedAchievements);
|
||||
expect(unlocked.some((a) => a.id === "GrowingStrong")).toBe(true);
|
||||
});
|
||||
|
||||
it("lockedAchievements excludes previously unlocked achievements", () => {
|
||||
const locked = get(lockedAchievements);
|
||||
expect(locked.some((a) => a.id === "GrowingStrong")).toBe(false);
|
||||
});
|
||||
|
||||
it("achievementProgress reflects the current unlocked count", () => {
|
||||
const progress = get(achievementProgress);
|
||||
expect(progress.unlocked).toBeGreaterThan(0);
|
||||
expect(progress.percentage).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("achievementsStore.updateProgress", () => {
|
||||
it("updates the progress value for an achievement", () => {
|
||||
achievementsStore.updateProgress("FirstMessage", 50);
|
||||
expect(get(achievementsStore).achievements["FirstMessage"].progress).toBe(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe("achievementsStore.reset", () => {
|
||||
it("resets totalUnlocked to 0 and lastUnlocked to null", () => {
|
||||
achievementsStore.reset();
|
||||
const state = get(achievementsStore);
|
||||
expect(state.totalUnlocked).toBe(0);
|
||||
expect(state.lastUnlocked).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("initAchievementsListener", () => {
|
||||
it("unlocks an achievement when the achievement:unlocked event fires", async () => {
|
||||
await initAchievementsListener();
|
||||
emitMockEvent("achievement:unlocked", makeEvent("FirstSteps"));
|
||||
expect(get(achievementsStore).achievements["FirstSteps"].unlocked).toBe(true);
|
||||
});
|
||||
|
||||
it("loads saved achievements from the backend without playing sounds", async () => {
|
||||
setMockInvokeResult("load_saved_achievements", [makeEvent("ConversationStarter")]);
|
||||
await initAchievementsListener();
|
||||
expect(get(achievementsStore).achievements["ConversationStarter"].unlocked).toBe(true);
|
||||
expect(playAchievementSound).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("logs an error when loading saved achievements fails", async () => {
|
||||
setMockInvokeResult("load_saved_achievements", new Error("Storage unavailable"));
|
||||
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
await initAchievementsListener();
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
"Failed to load saved achievements:",
|
||||
expect.any(Error)
|
||||
);
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -19,7 +19,6 @@
|
||||
* - [ ] Search query filters by content, language, and source
|
||||
*/
|
||||
|
||||
/* eslint-disable max-lines-per-function -- Test suites naturally have many cases */
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
// Mirror: detectLanguage from clipboard.ts
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import {
|
||||
configStore,
|
||||
maskPaths,
|
||||
@@ -7,6 +8,11 @@ import {
|
||||
applyTheme,
|
||||
applyCustomThemeColors,
|
||||
clearCustomThemeColors,
|
||||
isDarkTheme,
|
||||
isStreamerMode,
|
||||
isCompactMode,
|
||||
shouldHidePaths,
|
||||
showThinkingBlocks,
|
||||
MIN_FONT_SIZE,
|
||||
MAX_FONT_SIZE,
|
||||
DEFAULT_FONT_SIZE,
|
||||
@@ -843,4 +849,288 @@ describe("config store", () => {
|
||||
expect(lastSaved.font_size).toBe(16);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadConfig error path", () => {
|
||||
it("resets to default config and logs error when loadConfig fails", async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
await configStore.updateConfig({ theme: "light" });
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
vi.mocked(invoke).mockRejectedValue(new Error("Backend unavailable"));
|
||||
|
||||
await configStore.loadConfig();
|
||||
|
||||
expect(configStore.getConfig().theme).toBe("dark");
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to load config:", expect.any(Error));
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("configStore sidebar methods", () => {
|
||||
it("openSidebar sets isSidebarOpen to true", () => {
|
||||
configStore.closeSidebar();
|
||||
configStore.openSidebar();
|
||||
expect(get(configStore.isSidebarOpen)).toBe(true);
|
||||
});
|
||||
|
||||
it("closeSidebar sets isSidebarOpen to false", () => {
|
||||
configStore.openSidebar();
|
||||
configStore.closeSidebar();
|
||||
expect(get(configStore.isSidebarOpen)).toBe(false);
|
||||
});
|
||||
|
||||
it("toggleSidebar switches from false to true", () => {
|
||||
configStore.closeSidebar();
|
||||
configStore.toggleSidebar();
|
||||
expect(get(configStore.isSidebarOpen)).toBe(true);
|
||||
});
|
||||
|
||||
it("toggleSidebar switches from true to false", () => {
|
||||
configStore.openSidebar();
|
||||
configStore.toggleSidebar();
|
||||
expect(get(configStore.isSidebarOpen)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configStore setTheme method", () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("setTheme updates the theme via invoke", async () => {
|
||||
await configStore.setTheme("light");
|
||||
expect(configStore.getConfig().theme).toBe("light");
|
||||
});
|
||||
|
||||
it("setTheme with custom colors updates custom_theme_colors", async () => {
|
||||
const colors: CustomThemeColors = {
|
||||
bg_primary: "#001122",
|
||||
bg_secondary: null,
|
||||
bg_terminal: null,
|
||||
accent_primary: null,
|
||||
accent_secondary: null,
|
||||
text_primary: null,
|
||||
text_secondary: null,
|
||||
border_color: null,
|
||||
};
|
||||
|
||||
await configStore.setTheme("custom", colors);
|
||||
|
||||
expect(configStore.getConfig().theme).toBe("custom");
|
||||
expect(configStore.getConfig().custom_theme_colors.bg_primary).toBe("#001122");
|
||||
});
|
||||
});
|
||||
|
||||
describe("configStore setCustomThemeColors with custom theme active", () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
await configStore.setTheme("custom");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
clearCustomThemeColors();
|
||||
});
|
||||
|
||||
it("applies custom colors to DOM when current theme is custom", async () => {
|
||||
const colors: CustomThemeColors = {
|
||||
bg_primary: "#aabbcc",
|
||||
bg_secondary: null,
|
||||
bg_terminal: null,
|
||||
accent_primary: null,
|
||||
accent_secondary: null,
|
||||
text_primary: null,
|
||||
text_secondary: null,
|
||||
border_color: null,
|
||||
};
|
||||
|
||||
await configStore.setCustomThemeColors(colors);
|
||||
|
||||
expect(document.documentElement.style.getPropertyValue("--bg-primary")).toBe("#aabbcc");
|
||||
});
|
||||
});
|
||||
|
||||
describe("configStore font size methods", () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
await configStore.updateConfig({ font_size: DEFAULT_FONT_SIZE });
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("setFontSize updates to the given value", async () => {
|
||||
await configStore.setFontSize(18);
|
||||
expect(configStore.getConfig().font_size).toBe(18);
|
||||
});
|
||||
|
||||
it("setFontSize clamps to minimum", async () => {
|
||||
await configStore.setFontSize(1);
|
||||
expect(configStore.getConfig().font_size).toBe(MIN_FONT_SIZE);
|
||||
});
|
||||
|
||||
it("setFontSize clamps to maximum", async () => {
|
||||
await configStore.setFontSize(99);
|
||||
expect(configStore.getConfig().font_size).toBe(MAX_FONT_SIZE);
|
||||
});
|
||||
|
||||
it("increaseFontSize increases font size by 2", async () => {
|
||||
await configStore.increaseFontSize();
|
||||
expect(configStore.getConfig().font_size).toBe(DEFAULT_FONT_SIZE + 2);
|
||||
});
|
||||
|
||||
it("increaseFontSize does not exceed maximum", async () => {
|
||||
await configStore.setFontSize(MAX_FONT_SIZE);
|
||||
await configStore.increaseFontSize();
|
||||
expect(configStore.getConfig().font_size).toBe(MAX_FONT_SIZE);
|
||||
});
|
||||
|
||||
it("decreaseFontSize decreases font size by 2", async () => {
|
||||
await configStore.decreaseFontSize();
|
||||
expect(configStore.getConfig().font_size).toBe(DEFAULT_FONT_SIZE - 2);
|
||||
});
|
||||
|
||||
it("decreaseFontSize does not go below minimum", async () => {
|
||||
await configStore.setFontSize(MIN_FONT_SIZE);
|
||||
await configStore.decreaseFontSize();
|
||||
expect(configStore.getConfig().font_size).toBe(MIN_FONT_SIZE);
|
||||
});
|
||||
|
||||
it("resetFontSize restores the default font size", async () => {
|
||||
await configStore.setFontSize(20);
|
||||
await configStore.resetFontSize();
|
||||
expect(configStore.getConfig().font_size).toBe(DEFAULT_FONT_SIZE);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configStore removeAutoGrantedTool", () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
await configStore.updateConfig({ auto_granted_tools: [] });
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("removes an existing tool", async () => {
|
||||
await configStore.addAutoGrantedTool("Bash");
|
||||
await configStore.removeAutoGrantedTool("Bash");
|
||||
expect(configStore.getConfig().auto_granted_tools).not.toContain("Bash");
|
||||
});
|
||||
|
||||
it("is a no-op when the tool is not in the list", async () => {
|
||||
await configStore.removeAutoGrantedTool("NonExistentTool");
|
||||
expect(configStore.getConfig().auto_granted_tools).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configStore toggle methods", () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
await configStore.updateConfig({ streamer_mode: false, compact_mode: false });
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("toggleStreamerMode flips streamer_mode from false to true", async () => {
|
||||
await configStore.toggleStreamerMode();
|
||||
expect(configStore.getConfig().streamer_mode).toBe(true);
|
||||
});
|
||||
|
||||
it("toggleStreamerMode flips streamer_mode from true to false", async () => {
|
||||
await configStore.updateConfig({ streamer_mode: true });
|
||||
await configStore.toggleStreamerMode();
|
||||
expect(configStore.getConfig().streamer_mode).toBe(false);
|
||||
});
|
||||
|
||||
it("toggleCompactMode flips compact_mode from false to true", async () => {
|
||||
await configStore.toggleCompactMode();
|
||||
expect(configStore.getConfig().compact_mode).toBe(true);
|
||||
});
|
||||
|
||||
it("toggleCompactMode flips compact_mode from true to false", async () => {
|
||||
await configStore.updateConfig({ compact_mode: true });
|
||||
await configStore.toggleCompactMode();
|
||||
expect(configStore.getConfig().compact_mode).toBe(false);
|
||||
});
|
||||
|
||||
it("setCompactMode enables compact mode", async () => {
|
||||
await configStore.setCompactMode(true);
|
||||
expect(configStore.getConfig().compact_mode).toBe(true);
|
||||
});
|
||||
|
||||
it("setCompactMode disables compact mode", async () => {
|
||||
await configStore.updateConfig({ compact_mode: true });
|
||||
await configStore.setCompactMode(false);
|
||||
expect(configStore.getConfig().compact_mode).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("derived stores (live subscriptions)", () => {
|
||||
beforeEach(async () => {
|
||||
vi.mocked(invoke).mockResolvedValue(null);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("isDarkTheme is true when theme is dark", async () => {
|
||||
await configStore.updateConfig({ theme: "dark" });
|
||||
expect(get(isDarkTheme)).toBe(true);
|
||||
});
|
||||
|
||||
it("isDarkTheme is false when theme is not dark", async () => {
|
||||
await configStore.updateConfig({ theme: "light" });
|
||||
expect(get(isDarkTheme)).toBe(false);
|
||||
});
|
||||
|
||||
it("isStreamerMode reflects streamer_mode config", async () => {
|
||||
await configStore.updateConfig({ streamer_mode: true });
|
||||
expect(get(isStreamerMode)).toBe(true);
|
||||
await configStore.updateConfig({ streamer_mode: false });
|
||||
expect(get(isStreamerMode)).toBe(false);
|
||||
});
|
||||
|
||||
it("isCompactMode reflects compact_mode config", async () => {
|
||||
await configStore.updateConfig({ compact_mode: true });
|
||||
expect(get(isCompactMode)).toBe(true);
|
||||
await configStore.updateConfig({ compact_mode: false });
|
||||
expect(get(isCompactMode)).toBe(false);
|
||||
});
|
||||
|
||||
it("shouldHidePaths is true when both streamer flags are enabled", async () => {
|
||||
await configStore.updateConfig({ streamer_mode: true, streamer_hide_paths: true });
|
||||
expect(get(shouldHidePaths)).toBe(true);
|
||||
});
|
||||
|
||||
it("shouldHidePaths is false when streamer_mode is disabled", async () => {
|
||||
await configStore.updateConfig({ streamer_mode: false, streamer_hide_paths: true });
|
||||
expect(get(shouldHidePaths)).toBe(false);
|
||||
});
|
||||
|
||||
it("showThinkingBlocks is true when show_thinking_blocks is enabled", async () => {
|
||||
await configStore.updateConfig({ show_thinking_blocks: true });
|
||||
expect(get(showThinkingBlocks)).toBe(true);
|
||||
});
|
||||
|
||||
it("showThinkingBlocks is false when show_thinking_blocks is disabled", async () => {
|
||||
await configStore.updateConfig({ show_thinking_blocks: false });
|
||||
expect(get(showThinkingBlocks)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { setMockInvokeResult } from "../../../vitest.setup";
|
||||
import {
|
||||
formatCost,
|
||||
formatAlertType,
|
||||
getAlertMessage,
|
||||
costTrackingStore,
|
||||
formattedCosts,
|
||||
type AlertType,
|
||||
type CostAlert,
|
||||
} from "./costTracking";
|
||||
|
||||
vi.mock("$lib/notifications/notificationManager", () => ({
|
||||
notificationManager: {
|
||||
notifyCostAlert: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("formatCost", () => {
|
||||
it("formats amounts below $0.01 to 4 decimal places", () => {
|
||||
expect(formatCost(0)).toBe("$0.0000");
|
||||
@@ -95,3 +106,187 @@ describe("getAlertMessage", () => {
|
||||
expect(message).toContain("$0.0050");
|
||||
});
|
||||
});
|
||||
|
||||
describe("costTrackingStore", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
costTrackingStore.reset();
|
||||
});
|
||||
|
||||
describe("refresh", () => {
|
||||
it("loads cost data from the backend", async () => {
|
||||
setMockInvokeResult("get_today_cost", 1.5);
|
||||
setMockInvokeResult("get_week_cost", 5.25);
|
||||
setMockInvokeResult("get_month_cost", 20.0);
|
||||
setMockInvokeResult("get_cost_alerts", []);
|
||||
|
||||
await costTrackingStore.refresh();
|
||||
|
||||
const state = get(costTrackingStore);
|
||||
expect(state.todayCost).toBe(1.5);
|
||||
expect(state.weekCost).toBe(5.25);
|
||||
expect(state.monthCost).toBe(20.0);
|
||||
expect(state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it("triggers notifications for any returned alerts", async () => {
|
||||
const { notificationManager } = await import("$lib/notifications/notificationManager");
|
||||
const alert: CostAlert = { alert_type: "Daily", threshold: 1.0, current_cost: 1.5 };
|
||||
setMockInvokeResult("get_today_cost", 1.5);
|
||||
setMockInvokeResult("get_week_cost", 0);
|
||||
setMockInvokeResult("get_month_cost", 0);
|
||||
setMockInvokeResult("get_cost_alerts", [alert]);
|
||||
|
||||
await costTrackingStore.refresh();
|
||||
|
||||
expect(notificationManager.notifyCostAlert).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Today")
|
||||
);
|
||||
});
|
||||
|
||||
it("returns alerts from refresh", async () => {
|
||||
const alert: CostAlert = { alert_type: "Weekly", threshold: 5.0, current_cost: 6.0 };
|
||||
setMockInvokeResult("get_today_cost", 0);
|
||||
setMockInvokeResult("get_week_cost", 6.0);
|
||||
setMockInvokeResult("get_month_cost", 0);
|
||||
setMockInvokeResult("get_cost_alerts", [alert]);
|
||||
|
||||
const result = await costTrackingStore.refresh();
|
||||
|
||||
expect(result).toEqual([alert]);
|
||||
});
|
||||
|
||||
it("handles refresh errors gracefully and returns empty array", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("get_today_cost", new Error("Backend error"));
|
||||
|
||||
const result = await costTrackingStore.refresh();
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(errorSpy).toHaveBeenCalledWith("Failed to refresh cost tracking:", expect.any(Error));
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getSummary", () => {
|
||||
it("fetches and stores a cost summary", async () => {
|
||||
const mockSummary = {
|
||||
period_days: 7,
|
||||
total_input_tokens: 1000,
|
||||
total_output_tokens: 500,
|
||||
total_cost: 0.05,
|
||||
total_messages: 20,
|
||||
total_sessions: 3,
|
||||
average_daily_cost: 0.007,
|
||||
daily_breakdown: [],
|
||||
};
|
||||
setMockInvokeResult("get_cost_summary", mockSummary);
|
||||
|
||||
const result = await costTrackingStore.getSummary(7);
|
||||
|
||||
expect(result).toEqual(mockSummary);
|
||||
expect(invoke).toHaveBeenCalledWith("get_cost_summary", { days: 7 });
|
||||
});
|
||||
|
||||
it("returns null and logs error on failure", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("get_cost_summary", new Error("Summary unavailable"));
|
||||
|
||||
const result = await costTrackingStore.getSummary(30);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(errorSpy).toHaveBeenCalledWith("Failed to get cost summary:", expect.any(Error));
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAlertThresholds", () => {
|
||||
it("persists new thresholds to the backend", async () => {
|
||||
const thresholds = { daily: 2.0, weekly: 10.0, monthly: 40.0 };
|
||||
|
||||
await costTrackingStore.setAlertThresholds(thresholds);
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("set_cost_alert_thresholds", {
|
||||
daily: 2.0,
|
||||
weekly: 10.0,
|
||||
monthly: 40.0,
|
||||
});
|
||||
});
|
||||
|
||||
it("logs an error when the backend call fails", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("set_cost_alert_thresholds", new Error("Failed"));
|
||||
|
||||
await costTrackingStore.setAlertThresholds({ daily: 1.0, weekly: null, monthly: null });
|
||||
|
||||
expect(errorSpy).toHaveBeenCalledWith("Failed to set alert thresholds:", expect.any(Error));
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("exportCsv", () => {
|
||||
it("returns the CSV string from the backend", async () => {
|
||||
setMockInvokeResult("export_cost_csv", "date,cost\n2026-01-01,1.50");
|
||||
|
||||
const result = await costTrackingStore.exportCsv(7);
|
||||
|
||||
expect(result).toBe("date,cost\n2026-01-01,1.50");
|
||||
expect(invoke).toHaveBeenCalledWith("export_cost_csv", { days: 7 });
|
||||
});
|
||||
|
||||
it("returns null and logs error on failure", async () => {
|
||||
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
setMockInvokeResult("export_cost_csv", new Error("Export failed"));
|
||||
|
||||
const result = await costTrackingStore.exportCsv(30);
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(errorSpy).toHaveBeenCalledWith("Failed to export CSV:", expect.any(Error));
|
||||
errorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe("reset", () => {
|
||||
it("resets costs back to zero", async () => {
|
||||
setMockInvokeResult("get_today_cost", 5.0);
|
||||
setMockInvokeResult("get_week_cost", 15.0);
|
||||
setMockInvokeResult("get_month_cost", 50.0);
|
||||
setMockInvokeResult("get_cost_alerts", []);
|
||||
await costTrackingStore.refresh();
|
||||
|
||||
costTrackingStore.reset();
|
||||
|
||||
const state = get(costTrackingStore);
|
||||
expect(state.todayCost).toBe(0);
|
||||
expect(state.weekCost).toBe(0);
|
||||
expect(state.monthCost).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("formattedCosts", () => {
|
||||
beforeEach(() => {
|
||||
costTrackingStore.reset();
|
||||
});
|
||||
|
||||
it("formats the initial zero costs correctly", () => {
|
||||
const costs = get(formattedCosts);
|
||||
expect(costs.today).toBe("$0.0000");
|
||||
expect(costs.week).toBe("$0.0000");
|
||||
expect(costs.month).toBe("$0.0000");
|
||||
});
|
||||
|
||||
it("reflects updated costs after a refresh", async () => {
|
||||
setMockInvokeResult("get_today_cost", 1.5);
|
||||
setMockInvokeResult("get_week_cost", 5.0);
|
||||
setMockInvokeResult("get_month_cost", 20.0);
|
||||
setMockInvokeResult("get_cost_alerts", []);
|
||||
await costTrackingStore.refresh();
|
||||
|
||||
const costs = get(formattedCosts);
|
||||
expect(costs.today).toBe("$1.50");
|
||||
expect(costs.week).toBe("$5.00");
|
||||
expect(costs.month).toBe("$20.00");
|
||||
expect(costs.todayRaw).toBe(1.5);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { emitMockEvent } from "../../../vitest.setup";
|
||||
import { debugConsoleStore, filteredLogs } from "./debugConsole";
|
||||
|
||||
describe("debugConsoleStore", () => {
|
||||
beforeEach(() => {
|
||||
debugConsoleStore.clear();
|
||||
debugConsoleStore.close();
|
||||
debugConsoleStore.setFilterLevel("all");
|
||||
debugConsoleStore.setAutoScroll(true);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
debugConsoleStore.restoreConsole();
|
||||
debugConsoleStore.clear();
|
||||
});
|
||||
|
||||
it("initializes with correct default state", () => {
|
||||
const state = get(debugConsoleStore);
|
||||
expect(state.logs).toEqual([]);
|
||||
expect(state.isOpen).toBe(false);
|
||||
expect(state.maxLogs).toBe(1000);
|
||||
expect(state.filterLevel).toBe("all");
|
||||
expect(state.autoScroll).toBe(true);
|
||||
});
|
||||
|
||||
describe("toggle", () => {
|
||||
it("opens when currently closed", () => {
|
||||
debugConsoleStore.toggle();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it("closes when currently open", () => {
|
||||
debugConsoleStore.open();
|
||||
debugConsoleStore.toggle();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("open", () => {
|
||||
it("sets isOpen to true", () => {
|
||||
debugConsoleStore.open();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("close", () => {
|
||||
it("sets isOpen to false", () => {
|
||||
debugConsoleStore.open();
|
||||
debugConsoleStore.close();
|
||||
expect(get(debugConsoleStore).isOpen).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear", () => {
|
||||
it("removes all log entries", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
emitMockEvent("debug:log", { level: "info", message: "test entry" });
|
||||
expect(get(debugConsoleStore).logs.length).toBe(1);
|
||||
debugConsoleStore.clear();
|
||||
expect(get(debugConsoleStore).logs).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setFilterLevel", () => {
|
||||
it("updates filterLevel to the specified level", () => {
|
||||
debugConsoleStore.setFilterLevel("error");
|
||||
expect(get(debugConsoleStore).filterLevel).toBe("error");
|
||||
});
|
||||
|
||||
it("can reset filterLevel back to all", () => {
|
||||
debugConsoleStore.setFilterLevel("warn");
|
||||
debugConsoleStore.setFilterLevel("all");
|
||||
expect(get(debugConsoleStore).filterLevel).toBe("all");
|
||||
});
|
||||
});
|
||||
|
||||
describe("setAutoScroll", () => {
|
||||
it("disables autoScroll", () => {
|
||||
debugConsoleStore.setAutoScroll(false);
|
||||
expect(get(debugConsoleStore).autoScroll).toBe(false);
|
||||
});
|
||||
|
||||
it("re-enables autoScroll", () => {
|
||||
debugConsoleStore.setAutoScroll(false);
|
||||
debugConsoleStore.setAutoScroll(true);
|
||||
expect(get(debugConsoleStore).autoScroll).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupConsoleCapture", () => {
|
||||
afterEach(() => {
|
||||
debugConsoleStore.restoreConsole();
|
||||
});
|
||||
|
||||
it("captures console.log calls as info-level frontend logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.log("hello world");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "hello world");
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured?.level).toBe("info");
|
||||
expect(captured?.source).toBe("frontend");
|
||||
});
|
||||
|
||||
it("captures console.info calls as info-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.info("info message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "info message");
|
||||
expect(captured?.level).toBe("info");
|
||||
});
|
||||
|
||||
it("captures console.warn calls as warn-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.warn("warning message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "warning message");
|
||||
expect(captured?.level).toBe("warn");
|
||||
});
|
||||
|
||||
it("captures console.error calls as error-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.error("error message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "error message");
|
||||
expect(captured?.level).toBe("error");
|
||||
});
|
||||
|
||||
it("captures console.debug calls as debug-level logs", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.debug("debug message");
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "debug message");
|
||||
expect(captured?.level).toBe("debug");
|
||||
});
|
||||
|
||||
it("joins multiple console arguments with spaces", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
console.log("hello", "world", 42);
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find((l) => l.message === "hello world 42");
|
||||
expect(captured).toBeDefined();
|
||||
});
|
||||
|
||||
it("captures unhandled window error events", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
const errorEvent = new ErrorEvent("error", {
|
||||
message: "Test unhandled error",
|
||||
filename: "test.js",
|
||||
lineno: 10,
|
||||
colno: 5,
|
||||
});
|
||||
window.dispatchEvent(errorEvent);
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find(
|
||||
(l) => l.level === "error" && l.message.includes("[Unhandled Error]")
|
||||
);
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured?.message).toContain("Test unhandled error");
|
||||
});
|
||||
|
||||
it("captures unhandled promise rejection events", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
const rejectionEvent = Object.assign(new Event("unhandledrejection"), {
|
||||
reason: "test rejection reason",
|
||||
});
|
||||
window.dispatchEvent(rejectionEvent);
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
const captured = logs.find(
|
||||
(l) => l.level === "error" && l.message.includes("[Unhandled Promise Rejection]")
|
||||
);
|
||||
expect(captured).toBeDefined();
|
||||
expect(captured?.message).toContain("test rejection reason");
|
||||
});
|
||||
});
|
||||
|
||||
describe("restoreConsole", () => {
|
||||
it("stops capturing console output after restore", () => {
|
||||
debugConsoleStore.setupConsoleCapture();
|
||||
debugConsoleStore.restoreConsole();
|
||||
const countBefore = get(debugConsoleStore).logs.length;
|
||||
console.log("this should not be captured");
|
||||
expect(get(debugConsoleStore).logs.length).toBe(countBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe("setupBackendLogsListener", () => {
|
||||
it("captures backend logs emitted via debug:log event", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
emitMockEvent("debug:log", { level: "info", message: "backend message" });
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].level).toBe("info");
|
||||
expect(logs[0].message).toBe("backend message");
|
||||
expect(logs[0].source).toBe("backend");
|
||||
});
|
||||
|
||||
it("handles different log levels from backend", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
emitMockEvent("debug:log", { level: "error", message: "backend error" });
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
expect(logs[0].level).toBe("error");
|
||||
});
|
||||
});
|
||||
|
||||
describe("circular buffer", () => {
|
||||
it("drops oldest log when exceeding 1000-entry limit", async () => {
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
for (let i = 0; i < 1001; i++) {
|
||||
emitMockEvent("debug:log", { level: "info", message: `log ${i}` });
|
||||
}
|
||||
const logs = get(debugConsoleStore).logs;
|
||||
expect(logs.length).toBe(1000);
|
||||
expect(logs[0].message).toBe("log 1");
|
||||
expect(logs[999].message).toBe("log 1000");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("filteredLogs derived store", () => {
|
||||
beforeEach(async () => {
|
||||
debugConsoleStore.clear();
|
||||
debugConsoleStore.setFilterLevel("all");
|
||||
await debugConsoleStore.setupBackendLogsListener();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
debugConsoleStore.clear();
|
||||
});
|
||||
|
||||
it("returns all logs when filterLevel is all", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
expect(get(filteredLogs).length).toBe(4);
|
||||
});
|
||||
|
||||
it("returns only error logs when filterLevel is error", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("error");
|
||||
const logs = get(filteredLogs);
|
||||
expect(logs.length).toBe(1);
|
||||
expect(logs[0].level).toBe("error");
|
||||
});
|
||||
|
||||
it("returns warn and error logs when filterLevel is warn", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("warn");
|
||||
const logs = get(filteredLogs);
|
||||
expect(logs.length).toBe(2);
|
||||
expect(logs.every((l) => l.level === "warn" || l.level === "error")).toBe(true);
|
||||
});
|
||||
|
||||
it("excludes debug logs when filterLevel is info", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("info");
|
||||
const logs = get(filteredLogs);
|
||||
expect(logs.length).toBe(3);
|
||||
expect(logs.some((l) => l.level === "debug")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns all log levels when filterLevel is debug", () => {
|
||||
emitMockEvent("debug:log", { level: "debug", message: "d" });
|
||||
emitMockEvent("debug:log", { level: "info", message: "i" });
|
||||
emitMockEvent("debug:log", { level: "warn", message: "w" });
|
||||
emitMockEvent("debug:log", { level: "error", message: "e" });
|
||||
debugConsoleStore.setFilterLevel("debug");
|
||||
expect(get(filteredLogs).length).toBe(4);
|
||||
});
|
||||
});
|
||||
@@ -180,6 +180,16 @@ describe("draftsStore", () => {
|
||||
const result = draftsStore.formatTimestamp(invalid);
|
||||
expect(result).toBe(invalid);
|
||||
});
|
||||
|
||||
it("falls back to raw string when toLocaleString throws", () => {
|
||||
const spy = vi.spyOn(Date.prototype, "toLocaleString").mockImplementation(() => {
|
||||
throw new Error("Locale not supported");
|
||||
});
|
||||
const ts = "2026-01-15T14:30:00.000Z";
|
||||
const result = draftsStore.formatTimestamp(ts);
|
||||
expect(result).toBe(ts);
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import { get } from "svelte/store";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
stats,
|
||||
formattedStats,
|
||||
@@ -13,6 +15,7 @@ import {
|
||||
getBudgetStatusMessage,
|
||||
getRemainingTokenBudget,
|
||||
getRemainingCostBudget,
|
||||
initStatsListener,
|
||||
} from "./stats";
|
||||
import type { UsageStats, ToolTokenStats, BudgetStatus } from "./stats";
|
||||
|
||||
@@ -34,6 +37,12 @@ vi.mock("@tauri-apps/api/core", () => ({
|
||||
invoke: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./costTracking", () => ({
|
||||
costTrackingStore: {
|
||||
refresh: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
}));
|
||||
|
||||
describe("stats store", () => {
|
||||
beforeEach(() => {
|
||||
// Reset stats to default before each test
|
||||
@@ -801,4 +810,85 @@ describe("stats store", () => {
|
||||
expect(getRemainingCostBudget(baseStats, 0.2)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("initStatsListener", () => {
|
||||
const emptyStats: UsageStats = {
|
||||
total_input_tokens: 0,
|
||||
total_output_tokens: 0,
|
||||
total_cost_usd: 0,
|
||||
session_input_tokens: 0,
|
||||
session_output_tokens: 0,
|
||||
session_cost_usd: 0,
|
||||
model: null,
|
||||
messages_exchanged: 0,
|
||||
session_messages_exchanged: 0,
|
||||
code_blocks_generated: 0,
|
||||
session_code_blocks_generated: 0,
|
||||
files_edited: 0,
|
||||
session_files_edited: 0,
|
||||
files_created: 0,
|
||||
session_files_created: 0,
|
||||
tools_usage: {},
|
||||
session_tools_usage: {},
|
||||
session_duration_seconds: 0,
|
||||
context_tokens_used: 0,
|
||||
context_window_limit: 200000,
|
||||
context_utilisation_percent: 0,
|
||||
potential_cache_hits: 0,
|
||||
potential_cache_savings_tokens: 0,
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("registers a listener for the claude:stats event", async () => {
|
||||
vi.mocked(listen).mockResolvedValue(vi.fn());
|
||||
vi.mocked(invoke).mockResolvedValue(emptyStats);
|
||||
|
||||
await initStatsListener();
|
||||
|
||||
expect(listen).toHaveBeenCalledWith("claude:stats", expect.any(Function));
|
||||
});
|
||||
|
||||
it("updates the stats store when the listener callback fires", async () => {
|
||||
let capturedCallback: ((event: unknown) => void) | undefined;
|
||||
vi.mocked(listen).mockImplementation(async (_event, callback) => {
|
||||
capturedCallback = callback as (event: unknown) => void;
|
||||
return () => {};
|
||||
});
|
||||
vi.mocked(invoke).mockResolvedValue(emptyStats);
|
||||
|
||||
await initStatsListener();
|
||||
|
||||
const newStats = { ...emptyStats, total_input_tokens: 9999 };
|
||||
capturedCallback!({ payload: { stats: newStats } });
|
||||
|
||||
expect(get(stats).total_input_tokens).toBe(9999);
|
||||
});
|
||||
|
||||
it("loads persisted stats from the backend on initialisation", async () => {
|
||||
const persistedStats = { ...emptyStats, total_input_tokens: 5000 };
|
||||
vi.mocked(listen).mockResolvedValue(vi.fn());
|
||||
vi.mocked(invoke).mockResolvedValue(persistedStats);
|
||||
|
||||
await initStatsListener();
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("get_persisted_stats");
|
||||
expect(get(stats).total_input_tokens).toBe(5000);
|
||||
});
|
||||
|
||||
it("handles a failed invoke gracefully and logs the error", async () => {
|
||||
const error = new Error("Failed to get stats");
|
||||
vi.mocked(listen).mockResolvedValue(vi.fn());
|
||||
vi.mocked(invoke).mockRejectedValue(error);
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
|
||||
await initStatsListener();
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to load initial stats:", error);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* MessageMode Type Tests
|
||||
*
|
||||
* Tests the helper functions exported from messageMode.ts:
|
||||
* - getMessageMode: looks up a MessageMode by id
|
||||
* - formatMessageWithMode: prepends a mode prefix to a message
|
||||
*
|
||||
* What this module does:
|
||||
* - Defines the six conversation modes (chat, architect, code, debug, ask, review)
|
||||
* - Provides a lookup function to find a mode by its id
|
||||
* - Provides a formatting function that prepends the mode prefix to messages
|
||||
*
|
||||
* Manual testing checklist:
|
||||
* - [ ] Mode selector shows correct names and icons for all six modes
|
||||
* - [ ] Selecting a mode prefixes the next message with the correct prefix
|
||||
* - [ ] Switching back to Chat sends messages without any prefix
|
||||
* - [ ] Sending a message that already has the prefix does not double-prefix it
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { MESSAGE_MODES, getMessageMode, formatMessageWithMode } from "./messageMode";
|
||||
|
||||
describe("MESSAGE_MODES", () => {
|
||||
it("contains all six expected modes", () => {
|
||||
const ids = MESSAGE_MODES.map((m) => m.id);
|
||||
expect(ids).toEqual(["chat", "architect", "code", "debug", "ask", "review"]);
|
||||
});
|
||||
|
||||
it("each mode has an id, name, description, and icon", () => {
|
||||
for (const mode of MESSAGE_MODES) {
|
||||
expect(mode.id).toBeTruthy();
|
||||
expect(mode.name).toBeTruthy();
|
||||
expect(mode.description).toBeTruthy();
|
||||
expect(mode.icon).toBeTruthy();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMessageMode", () => {
|
||||
it("returns the chat mode with no prefix", () => {
|
||||
const mode = getMessageMode("chat");
|
||||
expect(mode).toBeDefined();
|
||||
expect(mode?.id).toBe("chat");
|
||||
expect(mode?.prefix).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns the architect mode with its prefix", () => {
|
||||
const mode = getMessageMode("architect");
|
||||
expect(mode?.id).toBe("architect");
|
||||
expect(mode?.prefix).toBe("[Architect Mode] ");
|
||||
});
|
||||
|
||||
it("returns the code mode with its prefix", () => {
|
||||
const mode = getMessageMode("code");
|
||||
expect(mode?.id).toBe("code");
|
||||
expect(mode?.prefix).toBe("[Code Mode] ");
|
||||
});
|
||||
|
||||
it("returns the debug mode with its prefix", () => {
|
||||
const mode = getMessageMode("debug");
|
||||
expect(mode?.id).toBe("debug");
|
||||
expect(mode?.prefix).toBe("[Debug Mode] ");
|
||||
});
|
||||
|
||||
it("returns the ask mode with its prefix", () => {
|
||||
const mode = getMessageMode("ask");
|
||||
expect(mode?.id).toBe("ask");
|
||||
expect(mode?.prefix).toBe("[Ask Mode] ");
|
||||
});
|
||||
|
||||
it("returns the review mode with its prefix", () => {
|
||||
const mode = getMessageMode("review");
|
||||
expect(mode?.id).toBe("review");
|
||||
expect(mode?.prefix).toBe("[Review Mode] ");
|
||||
});
|
||||
|
||||
it("returns undefined for an unknown mode id", () => {
|
||||
expect(getMessageMode("unknown")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns undefined for an empty string", () => {
|
||||
expect(getMessageMode("")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatMessageWithMode", () => {
|
||||
it("prepends the prefix for code mode", () => {
|
||||
expect(formatMessageWithMode("hello", "code")).toBe("[Code Mode] hello");
|
||||
});
|
||||
|
||||
it("prepends the prefix for architect mode", () => {
|
||||
expect(formatMessageWithMode("design this", "architect")).toBe("[Architect Mode] design this");
|
||||
});
|
||||
|
||||
it("prepends the prefix for debug mode", () => {
|
||||
expect(formatMessageWithMode("fix this bug", "debug")).toBe("[Debug Mode] fix this bug");
|
||||
});
|
||||
|
||||
it("prepends the prefix for ask mode", () => {
|
||||
expect(formatMessageWithMode("what is this?", "ask")).toBe("[Ask Mode] what is this?");
|
||||
});
|
||||
|
||||
it("prepends the prefix for review mode", () => {
|
||||
expect(formatMessageWithMode("review my code", "review")).toBe("[Review Mode] review my code");
|
||||
});
|
||||
|
||||
it("returns the message unchanged in chat mode (no prefix)", () => {
|
||||
expect(formatMessageWithMode("hello", "chat")).toBe("hello");
|
||||
});
|
||||
|
||||
it("returns the message unchanged for unknown mode ids", () => {
|
||||
expect(formatMessageWithMode("hello", "unknown")).toBe("hello");
|
||||
expect(formatMessageWithMode("hello", "")).toBe("hello");
|
||||
});
|
||||
|
||||
it("does not double-prefix a message already starting with the mode prefix", () => {
|
||||
expect(formatMessageWithMode("[Code Mode] already prefixed", "code")).toBe(
|
||||
"[Code Mode] already prefixed"
|
||||
);
|
||||
});
|
||||
|
||||
it("adds the prefix to an empty string", () => {
|
||||
expect(formatMessageWithMode("", "code")).toBe("[Code Mode] ");
|
||||
});
|
||||
});
|
||||
@@ -266,6 +266,30 @@ describe("stateMapper", () => {
|
||||
expect(mapMessageToState(message)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for content_block_start with unrecognised content block type", () => {
|
||||
const message = {
|
||||
type: "stream_event",
|
||||
event: {
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
content_block: { type: "input_json_delta" },
|
||||
},
|
||||
} as unknown as ClaudeStreamMessage;
|
||||
expect(mapMessageToState(message)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for content_block_delta with unrecognised delta type", () => {
|
||||
const message = {
|
||||
type: "stream_event",
|
||||
event: {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
delta: { type: "input_json_delta", partial_json: "{}" },
|
||||
},
|
||||
} as unknown as ClaudeStreamMessage;
|
||||
expect(mapMessageToState(message)).toBeNull();
|
||||
});
|
||||
|
||||
it("returns null for result with unknown subtype", () => {
|
||||
const message = {
|
||||
type: "result",
|
||||
|
||||
Reference in New Issue
Block a user