SPB Git

spb/khaelor Public

KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.

TypeScript 82.9% HTML 14.9% CSS 1.1% JavaScript 0.7%
4.5 KB · 125 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/tui/theme.test.ts4 * Description: Theme tests — capability ladder, NO_COLOR monochrome purity, light/dark variants.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { detectColorDepth } from "../../src/tui/renderer/capabilities.js";12import { resolveTheme } from "../../src/tui/theme.js";13import type { StyleRole } from "../../src/tui/theme.js";1415const ALL_ROLES: StyleRole[] = [16  "text",17  "dim",18  "accent",19  "accentDim",20  "success",21  "warning",22  "error",23  "code",24  "violet",25  "cyan",26  "magenta",27  "teal",28  "orange",29  "bold",30  "italic",31  "strike",32  "underline",33  "invert",34  "synKeyword",35  "synString",36  "synComment",37  "synNumber",38  "synFunction",39  "synType",40];4142describe("resolveTheme", () => {43  it("mono emits no SGR codes for any role (NO_COLOR honored absolutely)", () => {44    const mono = resolveTheme({ colorDepth: "mono" });45    for (const role of ALL_ROLES) {46      expect(mono.paint(role, "state")).toBe("state");47    }48  });4950  it("truecolor emits 24-bit foreground and closes with default-fg", () => {51    const dark = resolveTheme({ colorDepth: "truecolor" });52    const painted = dark.paint("accent", "x");53    expect(painted).toMatch(/^\x1b\[38;2;\d+;\d+;\d+mx\x1b\[39m$/);54  });5556  it("ansi256 uses indexed colors, ansi16 uses basic codes", () => {57    expect(resolveTheme({ colorDepth: "ansi256" }).paint("error", "x")).toContain("38;5;");58    expect(resolveTheme({ colorDepth: "ansi16" }).paint("error", "x")).toContain("\x1b[31m");59  });6061  it("text role in ansi16 stays the terminal default (no SGR)", () => {62    expect(resolveTheme({ colorDepth: "ansi16" }).paint("text", "x")).toBe("x");63  });6465  it("format roles use targeted close codes, never a full reset", () => {66    const dark = resolveTheme({ colorDepth: "truecolor" });67    expect(dark.paint("bold", "x")).toBe("\x1b[1mx\x1b[22m");68    expect(dark.paint("underline", "x")).toBe("\x1b[4mx\x1b[24m");69    expect(dark.paint("bold", "x")).not.toContain("\x1b[0m");70  });7172  it("selects the light variant for light backgrounds", () => {73    const light = resolveTheme({ colorDepth: "truecolor", background: "light" });74    expect(light.name).toBe("khaelor-light");75    expect(light.paint("text", "x")).not.toBe(76      resolveTheme({ colorDepth: "truecolor" }).paint("text", "x"),77    );78  });7980  it("painting an empty string emits nothing", () => {81    const dark = resolveTheme({ colorDepth: "truecolor" });82    expect(dark.paint("accent", "")).toBe("");83  });84});8586describe("gradients and tints", () => {87  it("brand gradient interpolates per character in truecolor and strips clean", () => {88    const dark = resolveTheme({ colorDepth: "truecolor" });89    const painted = dark.paintGradient("brand", "KHAELOR");90    const codes = painted.match(/\x1b\[38;2;\d+;\d+;\d+m/g) ?? [];91    expect(codes.length).toBe(7); // one 24-bit color per character92    expect(new Set(codes).size).toBeGreaterThan(1); // actually a gradient93    expect(painted.replace(/\x1b\[[0-9;]*m/g, "")).toBe("KHAELOR");94  });9596  it("gradient degrades to a solid brand color on ansi256 and to identity on mono", () => {97    const c256 = resolveTheme({ colorDepth: "ansi256" }).paintGradient("brand", "KHAELOR");98    expect(c256.match(/38;5;/g)?.length).toBe(1);99    expect(resolveTheme({ colorDepth: "mono" }).paintGradient("brand", "KHAELOR")).toBe("KHAELOR");100  });101102  it("line tints are truecolor-only backgrounds that close with bg-reset", () => {103    const dark = resolveTheme({ colorDepth: "truecolor" });104    expect(dark.tintLine("add", "x")).toMatch(/^\x1b\[48;2;\d+;\d+;\d+mx\x1b\[49m$/);105    expect(resolveTheme({ colorDepth: "ansi256" }).tintLine("add", "x")).toBe("x");106    expect(resolveTheme({ colorDepth: "mono" }).tintLine("del", "x")).toBe("x");107  });108});109110describe("detectColorDepth", () => {111  it("NO_COLOR and TERM=dumb force mono; non-TTY is mono", () => {112    expect(detectColorDepth({ NO_COLOR: "1", COLORTERM: "truecolor" }, true)).toBe("mono");113    expect(detectColorDepth({ TERM: "dumb" }, true)).toBe("mono");114    expect(detectColorDepth({ COLORTERM: "truecolor", TERM: "xterm" }, false)).toBe("mono");115  });116117  it("walks the ladder: truecolor → 256 → 16", () => {118    expect(detectColorDepth({ COLORTERM: "truecolor", TERM: "xterm-256color" }, true)).toBe(119      "truecolor",120    );121    expect(detectColorDepth({ TERM: "xterm-256color" }, true)).toBe("ansi256");122    expect(detectColorDepth({ TERM: "xterm" }, true)).toBe("ansi16");123  });124});125