/** * footer.test.ts — unit tests for the pipeline-overview footer (task: footer). * * The footer is presentation-only state: with no UI it tracks items but writes * nothing; with a stub UI it renders one status line and clears on `done()`. * These tests exercise the state machine (cursor demotion, terminal marks) * without spinning up a runner. */ import { describe, expect, it } from "bun:test"; import { createPipelineFooter, footerPhaseItems, FOOTER_GLYPH, FOOTER_STATUS_KEY, type ItemStatus, } from "../src/footer.js"; import { PHASE_LABELS } from "../src/phases.js"; import { PHASE_ANALYSIS, PHASE_FIX, PHASE_RECON } from "../src/run-state.js"; /** Minimal UI stub capturing `setStatus(key, text)` calls in order. */ function stubUi(): { ui: { setStatus: (key: string, text: string | undefined) => void }; calls: { key: string; text: string | undefined }[]; } { const calls: { key: string; text: string | undefined }[] = []; return { calls, ui: { setStatus(key, text) { calls.push({ key, text }); }, }, }; } /** Build the canonical phase-id list a scan-only check uses. */ function scanPhaseIds(): string[] { return [PHASE_RECON, PHASE_ANALYSIS, PHASE_FIX]; } describe("createPipelineFooter", () => { it("is a no-op without a UI but still tracks item state", () => { // hasUI false: setStatus must never be called. const footer = createPipelineFooter({ hasUI: false }); footer.setPipeline("pygienium smoke", [ { label: "Recon", status: "pending" }, ]); footer.setCursor(0); footer.done(); // No UI → no observable side effect, but getItems reflects state. expect(footer.getItems()[0]?.status).toBe("running"); expect(footer.getTitle()).toBe("pygienium smoke"); }); it("renders the full pipeline with the cursor running and clears on done", () => { const { ui, calls } = stubUi(); const footer = createPipelineFooter({ ui, hasUI: true }); const items = footerPhaseItems(scanPhaseIds(), PHASE_LABELS); footer.setPipeline("pygienium smoke", items, 0); // One setStatus call, under the canonical key, listing every phase: // cursor gets `▶