test: add coverage for messageMode, stateMapper branches, and initStatsListener

This commit is contained in:
2026-03-03 14:24:41 -08:00
committed by Naomi Carrigan
parent 7e45c685d3
commit acebe590c3
4 changed files with 240 additions and 2 deletions
-1
View File
@@ -19,7 +19,6 @@
* - [ ] Search query filters by content, language, and source * - [ ] 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"; import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mirror: detectLanguage from clipboard.ts // Mirror: detectLanguage from clipboard.ts
+91 -1
View File
@@ -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 { get } from "svelte/store";
import { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core";
import { import {
stats, stats,
formattedStats, formattedStats,
@@ -13,6 +15,7 @@ import {
getBudgetStatusMessage, getBudgetStatusMessage,
getRemainingTokenBudget, getRemainingTokenBudget,
getRemainingCostBudget, getRemainingCostBudget,
initStatsListener,
} from "./stats"; } from "./stats";
import type { UsageStats, ToolTokenStats, BudgetStatus } from "./stats"; import type { UsageStats, ToolTokenStats, BudgetStatus } from "./stats";
@@ -34,6 +37,12 @@ vi.mock("@tauri-apps/api/core", () => ({
invoke: vi.fn(), invoke: vi.fn(),
})); }));
vi.mock("./costTracking", () => ({
costTrackingStore: {
refresh: vi.fn().mockResolvedValue([]),
},
}));
describe("stats store", () => { describe("stats store", () => {
beforeEach(() => { beforeEach(() => {
// Reset stats to default before each test // Reset stats to default before each test
@@ -801,4 +810,85 @@ describe("stats store", () => {
expect(getRemainingCostBudget(baseStats, 0.2)).toBe(0); 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();
});
});
}); });
+125
View File
@@ -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] ");
});
});
+24
View File
@@ -266,6 +266,30 @@ describe("stateMapper", () => {
expect(mapMessageToState(message)).toBeNull(); 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", () => { it("returns null for result with unknown subtype", () => {
const message = { const message = {
type: "result", type: "result",