import { describe, expect, it } from "vitest"; import { FOOTER_FORMAT_ALIASES } from "../extensions/zentui/config"; import { collectFooterFormatReferences, compileCompactFormat, joinNonEmpty, parseFooterFormat, renderFormatSplit, renderFormatTokens, stripOrphanSeparators, } from "../extensions/zentui/footer-format"; describe("parseFooterFormat", () => { it("returns empty array for empty string", () => { expect(parseFooterFormat("")).toEqual([]); }); it("parses a single variable", () => { expect(parseFooterFormat("$cwd")).toEqual([{ kind: "var", name: "cwd" }]); }); it("parses model-info variables independently", () => { expect(parseFooterFormat("$model $provider")).toEqual([ { kind: "var", name: "model" }, { kind: "text", value: " " }, { kind: "var", name: "provider" }, ]); }); it("parses $package and $package_version as package-version vars", () => { expect(parseFooterFormat("$package")).toEqual([{ kind: "var", name: "package" }]); expect(parseFooterFormat("$package_version")).toEqual([ { kind: "var", name: "package_version" }, ]); }); it("parses $git_commit, $commit, $git_tag, and $tag as git-commit vars", () => { expect(parseFooterFormat("$git_commit")).toEqual([{ kind: "var", name: "git_commit" }]); expect(parseFooterFormat("$commit")).toEqual([{ kind: "var", name: "commit" }]); expect(parseFooterFormat("$git_tag")).toEqual([{ kind: "var", name: "git_tag" }]); expect(parseFooterFormat("$tag")).toEqual([{ kind: "var", name: "tag" }]); }); it("parses git-metrics vars", () => { expect(parseFooterFormat("$git_metrics")).toEqual([{ kind: "var", name: "git_metrics" }]); expect(parseFooterFormat("$git_added")).toEqual([{ kind: "var", name: "git_added" }]); expect(parseFooterFormat("$git_deleted")).toEqual([{ kind: "var", name: "git_deleted" }]); }); it("parses braced variables", () => { const braced = "${" + "git_branch}"; expect(parseFooterFormat(braced)).toEqual([{ kind: "var", name: "git_branch" }]); }); it("parses text and variables mixed", () => { expect(parseFooterFormat("$cwd on $git_branch")).toEqual([ { kind: "var", name: "cwd" }, { kind: "text", value: " on " }, { kind: "var", name: "git_branch" }, ]); }); it("parses the fill token", () => { expect(parseFooterFormat("$fill")).toEqual([{ kind: "fill" }]); }); it("preserves literal text exactly including multiple spaces", () => { const tokens = parseFooterFormat("$cwd on $git_branch"); expect(tokens).toEqual([ { kind: "var", name: "cwd" }, { kind: "text", value: " on " }, { kind: "var", name: "git_branch" }, ]); }); it("preserves leading and trailing text", () => { const tokens = parseFooterFormat("prefix $cwd suffix"); expect(tokens).toEqual([ { kind: "text", value: "prefix " }, { kind: "var", name: "cwd" }, { kind: "text", value: " suffix" }, ]); }); it("treats unknown variable names as var tokens", () => { expect(parseFooterFormat("$nope")).toEqual([{ kind: "var", name: "nope" }]); }); it("handles mixed fill, vars, and text", () => { expect(parseFooterFormat("$cwd on $git_branch $fill $cost $time")).toEqual([ { kind: "var", name: "cwd" }, { kind: "text", value: " on " }, { kind: "var", name: "git_branch" }, { kind: "text", value: " " }, { kind: "fill" }, { kind: "text", value: " " }, { kind: "var", name: "cost" }, { kind: "text", value: " " }, { kind: "var", name: "time" }, ]); }); }); describe("renderFormatSplit", () => { const renderVar = (name: string): string => { const map: Record = { cwd: "DIR", session_name: "Session name", git_branch: "BRANCH", cost: "$0", time: "12:00", unknown: "", }; return map[name] ?? ""; }; it("splits at the first fill token into left and right", () => { const tokens = parseFooterFormat("$cwd$fill$cost $time"); const { left, middle, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe("DIR"); expect(middle).toBe(""); expect(right).toBe("$0 12:00"); }); it("puts everything in left when there is no fill", () => { const tokens = parseFooterFormat("$cwd on $git_branch"); const { left, middle, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe("DIR on BRANCH"); expect(middle).toBe(""); expect(right).toBe(""); }); it("centers content between two fills in the middle zone", () => { const tokens = parseFooterFormat("$cwd$fill$cost$fill$time"); const { left, middle, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe("DIR"); expect(middle).toBe("$0"); expect(right).toBe("12:00"); }); it("ignores fill tokens beyond the first two", () => { const tokens = parseFooterFormat("$cwd$fill$cost$fill$time$fill"); const { left, middle, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe("DIR"); expect(middle).toBe("$0"); expect(right).toBe("12:00"); }); it("renders session-name variables in plain and braced forms", () => { expect(renderFormatSplit(parseFooterFormat("$session_name"), renderVar).left).toBe( "Session name", ); const bracedSessionName = "${" + "session_name}"; expect(renderFormatSplit(parseFooterFormat(bracedSessionName), renderVar).left).toBe( "Session name", ); }); it("renders unknown variables as empty string", () => { const tokens = parseFooterFormat("$nope"); const { left } = renderFormatSplit(tokens, renderVar); expect(left).toBe(""); }); it("renders literal text verbatim", () => { const tokens = parseFooterFormat("hello $cwd world"); const { left } = renderFormatSplit(tokens, renderVar); expect(left).toBe("hello DIR world"); }); it("handles empty token list", () => { const { left, middle, right } = renderFormatSplit([], renderVar); expect(left).toBe(""); expect(middle).toBe(""); expect(right).toBe(""); }); it("handles fill at the start", () => { const tokens = parseFooterFormat("$fill$cost"); const { left, middle, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe(""); expect(middle).toBe(""); expect(right).toBe("$0"); }); it("handles fill at the end", () => { const tokens = parseFooterFormat("$cwd$fill"); const { left, middle, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe("DIR"); expect(middle).toBe(""); expect(right).toBe(""); }); }); describe("conditional groups", () => { const renderVar = (name: string): string => { const map: Record = { cwd: "DIR", git_branch: "", git_status: "[!]", cost: "$0", runtime: "", }; return map[name] ?? ""; }; it("parses group tokens", () => { expect(parseFooterFormat("($git_branch)")).toEqual([ { kind: "group", tokens: [{ kind: "var", name: "git_branch" }] }, ]); }); it("drops a group when all vars are empty", () => { const tokens = parseFooterFormat("($git_branch)"); expect(renderFormatSplit(tokens, renderVar).left).toBe(""); }); it("drops surrounding literals inside an empty group", () => { const tokens = parseFooterFormat("($git_branch on )$cwd"); expect(renderFormatSplit(tokens, renderVar).left).toBe("DIR"); }); it("keeps a group when any var is non-empty", () => { const tokens = parseFooterFormat("($git_status)"); expect(renderFormatSplit(tokens, renderVar).left).toBe("[!]"); }); it("supports nested groups", () => { const tokens = parseFooterFormat("(($git_branch) via $runtime)$cwd"); expect(renderFormatSplit(tokens, renderVar).left).toBe("DIR"); }); it("still splits on fill outside groups", () => { const tokens = parseFooterFormat("$cwd$fill($cost)"); const { left, right } = renderFormatSplit(tokens, renderVar); expect(left).toBe("DIR"); expect(right).toBe("$0"); }); it("shows text-only groups", () => { const tokens = parseFooterFormat("(literal)$cwd"); expect(renderFormatSplit(tokens, renderVar).left).toBe("literalDIR"); }); it("treats unmatched top-level ) as literal and keeps trailing tokens", () => { const tokens = parseFooterFormat("$cwd) $cost"); expect(tokens).toEqual([ { kind: "var", name: "cwd" }, { kind: "text", value: ") " }, { kind: "var", name: "cost" }, ]); expect(renderFormatSplit(tokens, renderVar).left).toBe("DIR) $0"); }); it("keeps nested groups working when trailing tokens follow a closed group", () => { const tokens = parseFooterFormat("($git_status)$cwd"); expect(renderFormatSplit(tokens, renderVar).left).toBe("[!]DIR"); }); it("drops a conditional session-name group when the name is empty", () => { const noSessionName = (name: string) => (name === "sep" ? " | " : ""); const raw = renderFormatSplit(parseFooterFormat("($sep$session_name)"), noSessionName).left; expect(stripOrphanSeparators(raw)).toBe(""); }); }); describe("joinNonEmpty", () => { it("joins only non-empty parts", () => { expect(joinNonEmpty(["a", "", "b", ""], " | ")).toBe("a | b"); expect(joinNonEmpty(["", ""], " | ")).toBe(""); expect(joinNonEmpty(["only"], " | ")).toBe("only"); }); }); describe("compact footer format", () => { it("splits top-level wrap variables and records incoming boundary kinds", () => { const expected = [ { kind: "tokens", tokens: [{ kind: "var", name: "cwd" }], boundary: "space" }, { kind: "tokens", tokens: [{ kind: "var", name: "context" }], boundary: "space" }, ]; expect(compileCompactFormat(parseFooterFormat("$cwd$wrap$context"))).toEqual(expected); expect(compileCompactFormat(parseFooterFormat("$cwd$" + "{wrap}$context"))).toEqual(expected); expect( compileCompactFormat(parseFooterFormat("$context$wrap_sep$tokens")).map( ({ kind, boundary }) => ({ kind, boundary }), ), ).toEqual([ { kind: "tokens", boundary: "space" }, { kind: "tokens", boundary: "separator" }, ]); }); it("keeps nested boundaries empty without splitting and leaves wide rendering unchanged", () => { const [nested] = compileCompactFormat( parseFooterFormat("$cwd(foo $wrap $context bar $wrap_sep $tokens)"), ); expect(nested?.kind).toBe("tokens"); if (nested?.kind !== "tokens") return; expect( renderFormatTokens( nested.tokens, (name) => ({ cwd: "DIR", context: "CTX", tokens: "TOK" })[name] ?? "", ), ).toBe("DIRfoo CTX bar TOK"); expect( renderFormatSplit( parseFooterFormat("$cwd$wrap$context$wrap_sep$tokens"), (name) => ({ cwd: "DIR", context: "CTX", tokens: "TOK" })[name] ?? "", ).left, ).toBe("DIRCTXTOK"); }); it("ignores fill and recognizes only a standalone extensions chunk", () => { const chunks = compileCompactFormat( parseFooterFormat("$cwd$fill$wrap $extensions $wrap prefix $extensions$wrap($extensions)"), ); expect(chunks.map((chunk) => chunk.kind)).toEqual(["tokens", "extensions", "tokens", "tokens"]); }); it("renders conditional chunks and preserves incoming boundaries around empty content", () => { const chunks = compileCompactFormat( parseFooterFormat("$cwd$wrap_sep$missing$wrap(on $git_branch) $git_status$wrap_sep$context"), ); expect(chunks.map((chunk) => chunk.boundary)).toEqual([ "space", "separator", "space", "separator", ]); const rendered = chunks.flatMap((chunk) => chunk.kind === "tokens" ? [ { text: renderFormatTokens(chunk.tokens, (name) => name === "git_status" ? "[!]" : "", ).trim(), boundary: chunk.boundary, }, ] : [], ); expect(rendered).toEqual([ { text: "", boundary: "space" }, { text: "", boundary: "separator" }, { text: "[!]", boundary: "space" }, { text: "", boundary: "separator" }, ]); }); it("collects canonical data references and excludes layout variables", () => { expect( collectFooterFormatReferences( parseFooterFormat( "$directory $branch $model $provider $wrap $wrap_sep $extensions $fill $duration", ), FOOTER_FORMAT_ALIASES, ), ).toEqual(new Set(["cwd", "git_branch", "model", "provider", "session_duration"])); }); }); describe("stripOrphanSeparators", () => { it("strips leading and trailing plain pipe separators", () => { expect(stripOrphanSeparators(" | tokens")).toBe("tokens"); expect(stripOrphanSeparators("tokens | ")).toBe("tokens"); expect(stripOrphanSeparators(" | tokens | ")).toBe("tokens"); }); it("collapses repeated plain pipe separators", () => { // Two themed/plain seps in a row produce " | | " (space on both sides of each |). expect(stripOrphanSeparators("ctx | | tokens")).toBe("ctx | tokens"); expect(stripOrphanSeparators(" | | $0")).toBe("$0"); }); it("strips leading/trailing whitespace after empty groups drop", () => { expect(stripOrphanSeparators(" DIR")).toBe("DIR"); expect(stripOrphanSeparators("DIR ")).toBe("DIR"); expect(stripOrphanSeparators(" ")).toBe(""); }); it("handles ANSI-wrapped themed separators", () => { const sep = "\x1b[90m | \x1b[0m"; expect(stripOrphanSeparators(`${sep}tokens`)).toBe("tokens"); expect(stripOrphanSeparators(`ctx${sep}${sep}tokens`)).toBe(`ctx${sep}tokens`); expect(stripOrphanSeparators(`ctx${sep}tokens${sep}`)).toBe(`ctx${sep}tokens`); expect(stripOrphanSeparators(`${sep}tokens${sep}cost`)).toBe(`tokens${sep}cost`); }); it("does not destroy intentional pipes without surrounding spaces", () => { expect(stripOrphanSeparators("branch|feature")).toBe("branch|feature"); }); it("tidies right-side groups with empty middle metrics", () => { const renderVar = (name: string): string => { const map: Record = { context: "", tokens: "", cost: "$0", sep: " | ", }; return map[name] ?? ""; }; const tokens = parseFooterFormat("($context)($sep$tokens)($sep$cost)"); // Empty content vars drop their groups even when $sep is present. const raw = renderFormatSplit(tokens, renderVar).left; expect(raw).toBe(" | $0"); expect(stripOrphanSeparators(raw)).toBe("$0"); }); it("drops groups whose only non-empty var is $sep", () => { const renderVar = (name: string): string => (name === "sep" ? " | " : ""); const tokens = parseFooterFormat("($sep$tokens)cwd"); expect(renderFormatSplit(tokens, renderVar).left).toBe("cwd"); }); it("preserves themed separators between non-empty parts", () => { const sep = "\x1b[90m | \x1b[0m"; const renderVar = (name: string): string => { const map: Record = { context: "72%", tokens: "↑1", cost: "$0", sep, }; return map[name] ?? ""; }; const tokens = parseFooterFormat("($context)($sep$tokens)($sep$cost)"); const raw = renderFormatSplit(tokens, renderVar).left; expect(stripOrphanSeparators(raw)).toBe(`72%${sep}↑1${sep}$0`); }); });