generated from nhcarrigan/template
test: add coverage for messageMode, stateMapper branches, and initStatsListener
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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();
|
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",
|
||||||
|
|||||||
Reference in New Issue
Block a user