Files
hikari-desktop/src/lib/stores/debugConsole.test.ts
T
hikari fa906684c2
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 57s
CI / Lint & Test (push) Has been cancelled
CI / Build Linux (push) Has been cancelled
CI / Build Windows (cross-compile) (push) Has been cancelled
feat: multiple UI improvements, font settings, and memory file display names (#175)
## Summary

- **fix**: `show_thinking_blocks` setting now persists across sessions — it was defined on the TypeScript side but missing from the Rust `HikariConfig` struct, so serde silently dropped it on every save/load
- **feat**: Tool calls are now rendered as collapsible blocks matching the Extended Thinking block aesthetic, replacing the old inline dropdown approach
- **feat**: Add configurable max output tokens setting
- **feat**: Use random creative names for conversation tabs
- **test**: Significantly expanded frontend unit test coverage
- **docs**: Require tests for all changes in CLAUDE.md
- **feat**: Allow users to specify a custom terminal font (Closes #176)
- **feat**: Display friendly names for memory files derived from the first heading (Closes #177)
- **feat**: Add custom UI font support for the app chrome (buttons, labels, tabs)
- **fix**: Apply custom UI font to the full app interface — `.app-container` was hardcoded, blocking inheritance from `body`; also renamed "Custom Font" to "Custom Terminal Font" for clarity

 This PR was created with help from Hikari~ 🌸

Reviewed-on: #175
Co-authored-by: Hikari <hikari@nhcarrigan.com>
Co-committed-by: Hikari <hikari@nhcarrigan.com>
2026-03-03 20:21:58 -08:00

283 lines
10 KiB
TypeScript

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);
});
});