/** * @file Tests for the Workflows page's pipeline pass-rate panel: it renders one * row per node of the busiest pipeline, distinguishes a proved pass from a * merely claimed one, and switches templates through the picker. Conflating * evidence with a bare claim is the failure that matters here — it would make * an undocumented stage look verified. Also covers the per-node driver command: * shown and copyable when the template names one, absent when it does not. * @author Nguyễn Ngọc Trí Vĩ */ import { describe, it, expect, vi } from "vitest"; import { render, screen, fireEvent, waitFor, within } from "@testing-library/react"; import { LanePipelinePanel } from "../LanePipelinePanel"; const statsMock = vi.fn(); vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { lanes: { pipelineStats: (id?: number | null) => statsMock(id) } } }; }); const node = (id: string, over: Record = {}) => ({ id, label: id, icon: "🧭", gate: false, command: "", entered: 0, passed: 0, withEvidence: 0, failed: 0, detected: 0, ...over, }); const STATS = { pipelines: [ { id: "default", name: "Default feature pipeline", runs: 4, nodes: [ node("plan", { entered: 4, passed: 4, withEvidence: 4, command: "/superpowers:writing-plans", }), node("implement", { entered: 4, passed: 4, withEvidence: 0 }), node("tests", { entered: 3, passed: 2, withEvidence: 2, failed: 1 }), node("ship"), ], }, { id: "hotfix", name: "Hotfix", runs: 1, nodes: [node("patch", { entered: 1, passed: 1 })] }, ], lanes: [ { id: 1, title: "api lane", cwd: "/w/api" }, { id: 2, title: "", cwd: "/w/ui" }, ], }; describe("LanePipelinePanel per-node command", () => { it("shows the driver command under a node that declares one, and nothing for a node that does not", async () => { statsMock.mockResolvedValue(STATS); render(); const plan = await screen.findByTestId("pipeline-stat-command-plan"); expect(plan).toHaveTextContent("/superpowers:writing-plans"); // `tests` carries no command in the fixture: an empty string must render // nothing at all, never an empty clickable row. expect(screen.queryByTestId("pipeline-stat-command-tests")).toBeNull(); }); it("copies the command to the clipboard on click", async () => { const writeText = vi.fn().mockResolvedValue(undefined); Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true, }); statsMock.mockResolvedValue(STATS); render(); fireEvent.click(await screen.findByTestId("pipeline-stat-command-plan")); expect(writeText).toHaveBeenCalledWith("/superpowers:writing-plans"); }); }); describe("LanePipelinePanel", () => { it("shows pass count, entries and rate per node of the busiest pipeline", async () => { statsMock.mockResolvedValue(STATS); render(); const tests = await screen.findByTestId("pipeline-stat-tests"); expect(within(tests).getByText("2")).toBeInTheDocument(); expect(tests).toHaveTextContent("2/3"); expect(tests).toHaveTextContent("67%"); }); it("draws a claimed pass in a different colour from a proved one", async () => { statsMock.mockResolvedValue(STATS); render(); const proved = await screen.findByTestId("pipeline-stat-plan"); const claimed = screen.getByTestId("pipeline-stat-implement"); // Same counts, different bar: plan is all evidence, implement is all claim. expect(proved.querySelector(".bg-status-success")).toHaveStyle({ width: "100%" }); expect(claimed.querySelector(".bg-status-success")).toHaveStyle({ width: "0%" }); expect(claimed.querySelector(".bg-status-warning")).toHaveStyle({ width: "100%" }); }); it("marks a node that was never reached instead of showing 0%", async () => { statsMock.mockResolvedValue(STATS); render(); const ship = await screen.findByTestId("pipeline-stat-ship"); expect(ship).not.toHaveTextContent("%"); expect(ship).toHaveTextContent(/never reached/i); }); it("separates a node the hooks saw from one that never happened", async () => { // `gate` was reached in two runs but never declared. Reporting that as // "never reached" is what made the panel look broken to someone who works // without running the pipeline commands. statsMock.mockResolvedValue({ ...STATS, pipelines: STATS.pipelines.map((p, i) => i === 0 ? { ...p, nodes: [...p.nodes, node("gate", { detected: 2 })] } : p ), }); render(); const gate = await screen.findByTestId("pipeline-stat-gate"); expect(gate).not.toHaveTextContent(/never reached/i); expect(gate).toHaveTextContent("2"); }); it("switches templates through the picker", async () => { statsMock.mockResolvedValue(STATS); render(); fireEvent.change(await screen.findByTestId("pipeline-stats-picker"), { target: { value: "hotfix" }, }); expect(screen.getByTestId("pipeline-stat-patch")).toBeInTheDocument(); expect(screen.queryByTestId("pipeline-stat-plan")).not.toBeInTheDocument(); }); it("shows the CLI hint when nothing has been recorded", async () => { statsMock.mockResolvedValue({ pipelines: [], lanes: [] }); render(); expect(await screen.findByText(/agenthub workspace add/)).toBeInTheDocument(); }); it("refetches narrowed to the lane picked, and falls back to a cwd for an untitled one", async () => { statsMock.mockResolvedValue(STATS); render(); const picker = await screen.findByTestId("pipeline-stats-lane-picker"); expect(within(picker).getByText("/w/ui")).toBeInTheDocument(); expect(statsMock).toHaveBeenLastCalledWith(null); fireEvent.change(picker, { target: { value: "2" } }); expect(statsMock).toHaveBeenLastCalledWith(2); }); it("keeps the lane picker reachable when the picked lane recorded nothing", async () => { statsMock.mockResolvedValue(STATS); render(); // Without a picker in the empty branch, narrowing to a quiet lane would be // a one-way trip — no control left to select any other lane. statsMock.mockResolvedValue({ pipelines: [], lanes: STATS.lanes }); fireEvent.change(await screen.findByTestId("pipeline-stats-lane-picker"), { target: { value: "1" }, }); // Wait for the empty branch to take over, then re-query: the refetch swaps // the whole subtree, so a handle grabbed before it is already detached. await waitFor(() => expect(screen.queryByTestId("pipeline-stat-plan")).not.toBeInTheDocument()); expect(screen.getByTestId("pipeline-stats-lane-picker")).toBeInTheDocument(); expect(screen.queryByText(/agenthub workspace add/)).not.toBeInTheDocument(); }); it("surfaces a failed fetch", async () => { statsMock.mockRejectedValue(new Error("stats unreachable")); render(); expect(await screen.findByText("stats unreachable")).toBeInTheDocument(); }); });