import { describe, expect, it } from "vitest"; import { errorMessage, formatBytes, formatRelativeTime, recencyGroup, titleFromPrompt, truncate, } from "../lib/utils"; describe("truncate", () => { it("collapses whitespace so list rows never wrap unexpectedly", () => { expect(truncate("hello\n\n world", 40)).toBe("hello world"); }); it("leaves short text alone", () => { expect(truncate("short", 40)).toBe("short"); }); it("clips with an ellipsis at the requested width", () => { const result = truncate("a".repeat(100), 10); expect(result).toBe(`${"a".repeat(9)}…`); expect(result).toHaveLength(10); }); }); describe("formatRelativeTime", () => { const now = 1_700_000_000_000; it("reports very recent timestamps as just now", () => { expect(formatRelativeTime(now - 5_000, now)).toBe("just now"); }); it("reports minutes and hours", () => { expect(formatRelativeTime(now - 12 * 60_000, now)).toBe("12m ago"); expect(formatRelativeTime(now - 3 * 3_600_000, now)).toBe("3h ago"); }); it("reports days within the last week", () => { expect(formatRelativeTime(now - 2 * 86_400_000, now)).toBe("2d ago"); }); it("treats clock skew as just now rather than a negative age", () => { expect(formatRelativeTime(now + 60_000, now)).toBe("just now"); }); }); describe("recencyGroup", () => { // Fixed local noon so the day boundaries are unambiguous on any machine. const now = new Date(2026, 9, 4, 12, 0, 0).getTime(); const DAY = 86_400_000; it("buckets today", () => { expect(recencyGroup(now - 60_000, now)).toBe("Today"); }); it("buckets yesterday", () => { expect(recencyGroup(new Date(2026, 9, 3, 12, 0, 0).getTime(), now)).toBe("Yesterday"); }); it("buckets the rest of the week", () => { expect(recencyGroup(now - 3 * DAY, now)).toBe("This week"); expect(recencyGroup(now - 6 * DAY, now)).toBe("This week"); }); it("buckets anything older", () => { expect(recencyGroup(now - 30 * DAY, now)).toBe("Older"); }); it("does not bucket the small hours of today as yesterday", () => { // Just after midnight is still today, even though it is under 24h ago. const justAfterMidnight = new Date(2026, 9, 4, 0, 5, 0).getTime(); expect(recencyGroup(justAfterMidnight, now)).toBe("Today"); }); }); describe("titleFromPrompt", () => { it("uses the first non-empty line, stripped of markdown decoration", () => { expect(titleFromPrompt("\n\n## How do I deploy?\nmore text", 60)).toBe("How do I deploy?"); }); it("falls back to a placeholder for an empty prompt", () => { expect(titleFromPrompt(" \n ", 60)).toBe("New chat"); }); }); describe("errorMessage", () => { it("unwraps Error instances", () => { expect(errorMessage(new Error("boom"))).toBe("boom"); }); it("passes strings through", () => { expect(errorMessage("plain failure")).toBe("plain failure"); }); it("survives values that cannot be serialised", () => { const circular: Record = {}; circular.self = circular; expect(typeof errorMessage(circular)).toBe("string"); }); }); describe("formatBytes", () => { it("matches what `ollama list` prints for the same bytes", () => { // Captured from this machine: `ollama list` shows ornith-1.5:9b as 6.6 GB, // and a size that disagreed with the terminal would read as our bug. expect(formatBytes(6_550_813_657)).toBe("6.6 GB"); }); it("keeps small counts in bytes", () => { expect(formatBytes(0)).toBe("0 B"); expect(formatBytes(326)).toBe("326 B"); expect(formatBytes(999)).toBe("999 B"); }); it("steps up one unit at a time", () => { expect(formatBytes(1_500)).toBe("1.5 kB"); expect(formatBytes(2_500_000)).toBe("2.5 MB"); expect(formatBytes(3_000_000_000_000)).toBe("3.0 TB"); }); it("stops at terabytes rather than inventing a unit", () => { expect(formatBytes(9_000_000_000_000_000)).toBe("9000.0 TB"); }); it("has no answer for a size that is absent or nonsense", () => { // undefined rather than "NaN B", so a caller leaves the field out. expect(formatBytes(undefined)).toBeUndefined(); expect(formatBytes(-1)).toBeUndefined(); expect(formatBytes(Number.NaN)).toBeUndefined(); expect(formatBytes(Number.POSITIVE_INFINITY)).toBeUndefined(); }); });