import { describe, it, expect, beforeEach, vi } from "vitest"; import { get } from "svelte/store"; import { stats, formattedStats, resetSessionStats } from "./stats"; import type { UsageStats } from "./stats"; // Mock Tauri APIs vi.mock("@tauri-apps/api/event", () => ({ listen: vi.fn(), })); vi.mock("@tauri-apps/api/core", () => ({ invoke: vi.fn(), })); describe("stats store", () => { beforeEach(() => { // Reset stats to default before each test stats.set({ 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, }); }); describe("stats writable store", () => { it("has correct default values", () => { const currentStats = get(stats); expect(currentStats.total_input_tokens).toBe(0); expect(currentStats.total_output_tokens).toBe(0); expect(currentStats.total_cost_usd).toBe(0); expect(currentStats.model).toBeNull(); }); it("can be updated with set", () => { const newStats: UsageStats = { total_input_tokens: 1000, total_output_tokens: 2000, total_cost_usd: 0.05, session_input_tokens: 500, session_output_tokens: 1000, session_cost_usd: 0.025, model: "claude-sonnet-4", messages_exchanged: 10, session_messages_exchanged: 5, code_blocks_generated: 3, session_code_blocks_generated: 2, files_edited: 5, session_files_edited: 2, files_created: 1, session_files_created: 1, tools_usage: { Read: 5, Edit: 3 }, session_tools_usage: { Read: 2, Edit: 1 }, session_duration_seconds: 300, }; stats.set(newStats); const currentStats = get(stats); expect(currentStats.total_input_tokens).toBe(1000); expect(currentStats.total_output_tokens).toBe(2000); expect(currentStats.model).toBe("claude-sonnet-4"); expect(currentStats.tools_usage).toEqual({ Read: 5, Edit: 3 }); }); it("can be updated with update function", () => { stats.update((current) => ({ ...current, total_input_tokens: 500, session_messages_exchanged: 3, })); const currentStats = get(stats); expect(currentStats.total_input_tokens).toBe(500); expect(currentStats.session_messages_exchanged).toBe(3); }); }); describe("resetSessionStats", () => { it("resets all session fields to zero", () => { // First set some values stats.set({ total_input_tokens: 1000, total_output_tokens: 2000, total_cost_usd: 0.05, session_input_tokens: 500, session_output_tokens: 1000, session_cost_usd: 0.025, model: "claude-sonnet-4", messages_exchanged: 10, session_messages_exchanged: 5, code_blocks_generated: 3, session_code_blocks_generated: 2, files_edited: 5, session_files_edited: 2, files_created: 1, session_files_created: 1, tools_usage: { Read: 5, Edit: 3 }, session_tools_usage: { Read: 2, Edit: 1 }, session_duration_seconds: 300, }); // Reset session stats resetSessionStats(); const currentStats = get(stats); // Total stats should be preserved expect(currentStats.total_input_tokens).toBe(1000); expect(currentStats.total_output_tokens).toBe(2000); expect(currentStats.total_cost_usd).toBe(0.05); expect(currentStats.messages_exchanged).toBe(10); expect(currentStats.code_blocks_generated).toBe(3); expect(currentStats.files_edited).toBe(5); expect(currentStats.files_created).toBe(1); expect(currentStats.tools_usage).toEqual({ Read: 5, Edit: 3 }); expect(currentStats.model).toBe("claude-sonnet-4"); // Session stats should be reset expect(currentStats.session_input_tokens).toBe(0); expect(currentStats.session_output_tokens).toBe(0); expect(currentStats.session_cost_usd).toBe(0); expect(currentStats.session_messages_exchanged).toBe(0); expect(currentStats.session_code_blocks_generated).toBe(0); expect(currentStats.session_files_edited).toBe(0); expect(currentStats.session_files_created).toBe(0); expect(currentStats.session_tools_usage).toEqual({}); expect(currentStats.session_duration_seconds).toBe(0); }); }); describe("formattedStats derived store", () => { it("formats token numbers with locale string", () => { stats.update((current) => ({ ...current, total_input_tokens: 1234567, total_output_tokens: 7654321, session_input_tokens: 12345, session_output_tokens: 54321, })); const formatted = get(formattedStats); expect(formatted.totalTokens).toBe("8,888,888"); expect(formatted.totalInputTokens).toBe("1,234,567"); expect(formatted.totalOutputTokens).toBe("7,654,321"); expect(formatted.sessionTokens).toBe("66,666"); expect(formatted.sessionInputTokens).toBe("12,345"); expect(formatted.sessionOutputTokens).toBe("54,321"); }); it("formats cost with 4 decimal places", () => { stats.update((current) => ({ ...current, total_cost_usd: 1.23456, session_cost_usd: 0.00123, })); const formatted = get(formattedStats); expect(formatted.totalCost).toBe("$1.2346"); expect(formatted.sessionCost).toBe("$0.0012"); }); it("formats duration seconds only", () => { stats.update((current) => ({ ...current, session_duration_seconds: 45, })); const formatted = get(formattedStats); expect(formatted.sessionDuration).toBe("45s"); }); it("formats duration minutes and seconds", () => { stats.update((current) => ({ ...current, session_duration_seconds: 125, // 2m 5s })); const formatted = get(formattedStats); expect(formatted.sessionDuration).toBe("2m 5s"); }); it("formats duration hours, minutes, and seconds", () => { stats.update((current) => ({ ...current, session_duration_seconds: 3725, // 1h 2m 5s })); const formatted = get(formattedStats); expect(formatted.sessionDuration).toBe("1h 2m 5s"); }); it("formats duration with zero seconds", () => { stats.update((current) => ({ ...current, session_duration_seconds: 3600, // exactly 1h })); const formatted = get(formattedStats); expect(formatted.sessionDuration).toBe("1h 0m 0s"); }); it("shows model name when available", () => { stats.update((current) => ({ ...current, model: "claude-opus-4-5", })); const formatted = get(formattedStats); expect(formatted.model).toBe("claude-opus-4-5"); }); it("shows placeholder when model is null", () => { stats.update((current) => ({ ...current, model: null, })); const formatted = get(formattedStats); expect(formatted.model).toBe("No model selected"); }); it("formats message counts", () => { stats.update((current) => ({ ...current, messages_exchanged: 100, session_messages_exchanged: 10, })); const formatted = get(formattedStats); expect(formatted.messagesTotal).toBe("100"); expect(formatted.messagesSession).toBe("10"); }); it("formats code block counts", () => { stats.update((current) => ({ ...current, code_blocks_generated: 50, session_code_blocks_generated: 5, })); const formatted = get(formattedStats); expect(formatted.codeBlocksTotal).toBe("50"); expect(formatted.codeBlocksSession).toBe("5"); }); it("formats file counts", () => { stats.update((current) => ({ ...current, files_edited: 25, session_files_edited: 3, files_created: 10, session_files_created: 2, })); const formatted = get(formattedStats); expect(formatted.filesEditedTotal).toBe("25"); expect(formatted.filesEditedSession).toBe("3"); expect(formatted.filesCreatedTotal).toBe("10"); expect(formatted.filesCreatedSession).toBe("2"); }); it("exposes tools usage directly", () => { const toolsUsage = { Read: 10, Edit: 5, Write: 3 }; const sessionToolsUsage = { Read: 2, Edit: 1 }; stats.update((current) => ({ ...current, tools_usage: toolsUsage, session_tools_usage: sessionToolsUsage, })); const formatted = get(formattedStats); expect(formatted.toolsUsage).toEqual(toolsUsage); expect(formatted.sessionToolsUsage).toEqual(sessionToolsUsage); }); it("handles zero values correctly", () => { const formatted = get(formattedStats); expect(formatted.totalTokens).toBe("0"); expect(formatted.totalCost).toBe("$0.0000"); expect(formatted.sessionDuration).toBe("0s"); expect(formatted.messagesTotal).toBe("0"); }); it("handles large numbers with proper formatting", () => { stats.update((current) => ({ ...current, total_input_tokens: 1000000000, // 1 billion messages_exchanged: 999999, })); const formatted = get(formattedStats); expect(formatted.totalInputTokens).toBe("1,000,000,000"); expect(formatted.messagesTotal).toBe("999,999"); }); }); describe("UsageStats interface", () => { it("supports all expected fields", () => { const fullStats: UsageStats = { total_input_tokens: 100, total_output_tokens: 200, total_cost_usd: 0.01, session_input_tokens: 50, session_output_tokens: 100, session_cost_usd: 0.005, model: "test-model", messages_exchanged: 5, session_messages_exchanged: 2, code_blocks_generated: 3, session_code_blocks_generated: 1, files_edited: 2, session_files_edited: 1, files_created: 1, session_files_created: 0, tools_usage: { Read: 3 }, session_tools_usage: { Read: 1 }, session_duration_seconds: 60, }; stats.set(fullStats); const currentStats = get(stats); // Verify all fields are present and correct expect(currentStats).toEqual(fullStats); }); }); });