generated from nhcarrigan/template
fa906684c2
## 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>
283 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|