import { describe, expect, it } from "vitest"; import { FOOTER_FORMAT_ALIASES } from "../extensions/zentui/config"; import { collectFooterFormatReferences, compileCompactFormat, compileCompactFormatSplit, joinNonEmpty, parseFooterFormat, renderFormatSplit, renderFormatTokens, stripOrphanSeparators, } from "../extensions/zentui/footer-format"; import { packCompactChunks } from "../extensions/zentui/footer-layout"; 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 bare and braced thinking levels in conditional groups", () => { for (const variable of ["$thinkingLevel", "$" + "{thinkingLevel}"]) { expect(parseFooterFormat(variable)).toEqual([{ kind: "var", name: "thinkingLevel" }]); expect(parseFooterFormat(`( ${variable})`)).toEqual([ { kind: "group", tokens: [ { kind: "text", value: " " }, { kind: "var", name: "thinkingLevel" }, ], }, ]); } }); 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("recognizes only a standalone extensions chunk", () => { const chunks = compileCompactFormat( parseFooterFormat("$cwd$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( $thinkingLevel)( $" + "{thinkingLevel}) $wrap $wrap_sep $extensions $fill $duration", ), FOOTER_FORMAT_ALIASES, ), ).toEqual( new Set(["cwd", "git_branch", "model", "provider", "thinkingLevel", "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`); }); }); describe("compact fill zones", () => { it.each(["$fill", "$" + "{fill}"])("flushes chunks and resets boundaries at %s", (fill) => { const split = compileCompactFormatSplit(parseFooterFormat(`$cwd$wrap_sep${fill}$model`)); expect(split.alignRight).toBe(true); expect(split.left).toEqual(compileCompactFormat(parseFooterFormat("$cwd"))); expect(split.right).toEqual(compileCompactFormat(parseFooterFormat("$model"))); }); it("preserves no-fill compilation and ignores nested and additional fills", () => { const tokens = parseFooterFormat("$cwd($fill$model)$wrap$extensions"); expect(compileCompactFormatSplit(tokens)).toEqual({ left: compileCompactFormat(tokens), right: [], alignRight: false, }); const split = compileCompactFormatSplit( parseFooterFormat("$cwd$fill$model$fill$provider$wrap_sep$extensions"), ); expect(split.right).toEqual( compileCompactFormat(parseFooterFormat("$model$provider$wrap_sep$extensions")), ); expect(split.right.at(-1)).toEqual({ kind: "extensions", boundary: "separator" }); }); it("supports empty zones and boundaries immediately after fill", () => { expect(compileCompactFormatSplit(parseFooterFormat("$fill"))).toEqual({ left: [], right: [], alignRight: true, }); expect(compileCompactFormatSplit(parseFooterFormat("$cwd$fill")).right).toEqual([]); const split = compileCompactFormatSplit(parseFooterFormat("$fill$wrap_sep$extensions")); expect(split.left).toEqual([]); expect(split.right).toEqual([{ kind: "extensions", boundary: "separator" }]); }); }); describe("conditional join separator", () => { const sep = "\x1b[90m | \x1b[0m"; const render = (format: string, values: Record = {}) => renderFormatTokens(parseFooterFormat(format), (name) => { if (name === "join_sep") throw new Error("marker must not resolve data"); return name === "sep" ? sep : (values[name] ?? ""); }); it.each(["$join_sep", "$" + "{join_sep}"])( "parses %s as an ordinary variable and joins all presence combinations", (marker) => { expect(parseFooterFormat(marker)).toEqual([{ kind: "var", name: "join_sep" }]); for (let mask = 0; mask < 8; mask++) { const values = Object.fromEntries( ["a", "b", "c"].map((name, index) => [ name, mask & (1 << index) ? name.toUpperCase() : "", ]), ); expect(render(`$a${marker}$b${marker}$c`, values)).toBe( Object.values(values).filter(Boolean).join(sep), ); } for (const values of [{}, { a: "A" }, { b: "B" }, { a: "A", b: "B" }]) expect(render(`$a${marker}$b`, values)).toBe(Object.values(values).join(sep)); }, ); it("drops edge, consecutive, unknown, whitespace and control-only fields without changing retained output", () => { const linked = "\x1b[31m\x1b]8;;https://example.com\x1b\\界\ue0a0\x1b]8;;\x1b\\\x1b[0m"; for (const empty of [ "", " \t ", "\x1b[31m \x1b[0m", "\x1b[0m", "\x1b]8;;https://example.com\x07\x1b]8;;\x07", ]) expect( render("$join_sep$missing$join_sep$a$join_sep$join_sep$b$join_sep$c$join_sep", { a: linked, b: empty, c: " C ", }), ).toBe(`${linked}${sep} C `); expect(render("literal $join_sep$a", { a: " A " })).toBe(`literal ${sep} A `); expect(render("$join_sep$missing$join_sep")).toBe(""); }); it("keeps optional-group liveness and nested sibling scopes local", () => { expect(render("$a$join_sep($b$join_sep($c$join_sep$d))", { a: "A", c: "C", d: "D" })).toBe( `A${sep}C${sep}D`, ); expect(render("$a($join_sep$b)", { a: "A", b: "B" })).toBe("AB"); expect(render("$a$join_sep(prefix $missing$join_sep suffix)", { a: "A" })).toBe("A"); expect(render("$a$join_sep( $join_sep )", { a: "A" })).toBe("A"); expect(render("(prefix ($a$join_sep$b))", { a: "\x1b[0m", b: " " })).toBe(""); expect(render("(literal$join_sep other)")).toBe(`literal${sep} other`); expect(render("($a$fill$join_sep$b)", { a: "A", b: "B" })).toBe(`A${sep}B`); }); it("cannot join across fill zones or compact chunks; wrap_sep stays packer-owned", () => { const resolve = (name: string) => ({ a: "A", b: "B", c: "C", sep })[name] ?? ""; expect( renderFormatSplit(parseFooterFormat("$a$join_sep$fill$join_sep$b$fill$join_sep$c"), resolve), ).toEqual({ left: "A", middle: "B", right: "C" }); const chunks = compileCompactFormat( parseFooterFormat("$a$join_sep$wrap$join_sep$b$wrap_sep$join_sep$c"), ); expect( chunks.map((chunk) => chunk.kind === "tokens" ? renderFormatTokens(chunk.tokens, resolve) : "", ), ).toEqual(["A", "B", "C"]); expect(chunks.map((chunk) => chunk.boundary)).toEqual(["space", "space", "separator"]); const packed = chunks.flatMap((chunk) => chunk.kind === "tokens" ? [{ text: renderFormatTokens(chunk.tokens, resolve), boundary: chunk.boundary }] : [], ); expect(packCompactChunks(packed, 10, 3, " | ")).toEqual(["A B | C"]); expect(packCompactChunks(packed, 5, 3, " | ")).toEqual(["A B", "C"]); }); it("does not change legacy sep, whitespace or resolver behavior when no marker is present", () => { expect(render(" $a $sep $missing ", { a: "A" })).toBe(` A ${sep} `); expect(render("$a", { a: "\x1b[31m \x1b[0m" })).toBe("\x1b[31m \x1b[0m"); expect( renderFormatTokens(parseFooterFormat("$a$join_sep$b"), (name) => { if (name === "sep") throw new Error("unneeded separator"); return name === "a" ? "A" : ""; }), ).toBe("A"); }); });