import type { Theme } from "@earendil-works/pi-coding-agent"; import { getOsc8LinkAtColumn, stripTerminalSequences, visibleWidth, } from "@earendil-works/pi-tui"; import { describe, expect, it } from "vitest"; import { createStrataWidget, renderStrataWidget, STRATA_ICON, type StrataPhase, type StrataWidgetState, } from "../widget.js"; function testTheme() { let generation = 0; const calls: Array<{ color: string; text: string }> = []; const theme = { bold: (text: string) => `\x1b[1m${text}\x1b[22m`, fg: (color: string, text: string) => { calls.push({ color, text }); return `\x1b[${31 + generation}m${text}\x1b[39m`; }, } as Theme; return { theme, calls, change: () => generation++ }; } function readyState(): StrataWidgetState { return { phase: "Ready", source: { kind: "base", ref: "trunk" }, url: "http://127.0.0.1:43210/?token=trusted", reviewed: 8, totalHunks: 24, layers: 4, comments: 2, }; } describe("Strata Pi widget", () => { it("renders the verified icon, semantic state, counts, action, and full link target", () => { const { theme } = testTheme(); const lines = renderStrataWidget(readyState(), theme, 160); const visible = lines.map(stripTerminalSequences); expect(visible).toEqual([ `▏ ${STRATA_ICON} STRATA ● Ready · base trunk · 8/24 reviewed · 4 layers · 2 comments`, "▏ http://127.0.0.1:43210 /strata cancel", ]); expect(lines[1]).toContain("\x1b]8;;http://127.0.0.1:43210/?token=trusted"); expect(visible.join("\n")).not.toContain("full-authentication-token"); }); it("bounds every row and drops secondary details before state and access", () => { const { theme } = testTheme(); for (const width of [1, 4, 12, 24, 36, 48, 80, 160]) { const lines = renderStrataWidget(readyState(), theme, width); expect(lines).toHaveLength(2); for (const line of lines) expect(visibleWidth(line)).toBeLessThanOrEqual(width); } const narrow = renderStrataWidget(readyState(), theme, 36).map( stripTerminalSequences, ); expect(narrow[0]).toContain("Ready"); expect(narrow[0]).not.toContain("comments"); expect(narrow[1]).toContain("http://127.0.0.1:43210"); }); it("renders every lifecycle state with semantic color and its contextual action", () => { const { theme, calls } = testTheme(); const cases: Array<[StrataPhase, string, string]> = [ ["Collecting", "Capturing Git snapshot · /strata cancel", "accent"], ["Planning", "Organizing review layers · /strata cancel", "accent"], ["Ready", "/strata cancel", "success"], ["Answering", "Answering browser question", "accent"], ["Refreshing", "Refreshing snapshot", "accent"], ["Stale", "Refresh in browser", "warning"], ["Feedback sent", "Feedback queued for Pi", "success"], ["Error", "provider failed", "error"], ["Closed", "Run /strata to reopen", "muted"], ]; for (const [phase, action, color] of cases) { calls.length = 0; const lines = renderStrataWidget( { ...readyState(), phase, ...(phase === "Error" ? { error: "provider failed" } : {}), ...(phase === "Closed" ? { url: undefined } : {}), }, theme, 160, ).map(stripTerminalSequences); expect(lines[0]).toContain(phase); expect(lines[1]).toContain(action); expect(calls).toContainEqual({ color, text: `● ${phase}` }); } }); it("drops the collecting and planning cancel hint when width is tight", () => { const { theme } = testTheme(); for (const phase of ["Collecting", "Planning"] as const) { const wide = renderStrataWidget({ phase }, theme, 80).map( stripTerminalSequences, ); const narrow = renderStrataWidget({ phase }, theme, 28).map( stripTerminalSequences, ); expect(wide[1]).toContain("/strata cancel"); expect(narrow[1]).not.toContain("/strata cancel"); } }); it("neutralizes terminal controls without changing the trusted authenticated target", () => { const { theme } = testTheme(); const trusted = "http://127.0.0.1:43210/?token=trusted"; const lines = renderStrataWidget( { ...readyState(), phase: "Error", url: trusted, source: { kind: "base", ref: "bad\x1b]8;;https://evil.example\x07ref" }, error: "failed\r\n\x1b]8;;https://evil.example\x07click\x1b]8;;\x07", }, theme, 240, ); const links = lines.flatMap((line) => Array.from({ length: visibleWidth(line) }, (_, column) => getOsc8LinkAtColumn(line, column), ).filter((value): value is string => value !== undefined), ); expect(new Set(links)).toEqual(new Set([trusted])); expect(stripTerminalSequences(lines.join("\n"))).not.toMatch( /[\r\n].*[\r\n]/, ); }); it("recomputes theme styling after invalidation", () => { const { theme, change } = testTheme(); const widget = createStrataWidget(readyState(), theme); const before = widget.render(120); change(); widget.invalidate(); const after = widget.render(120); expect(after).not.toEqual(before); expect(after.map(stripTerminalSequences)).toEqual( before.map(stripTerminalSequences), ); }); });