From b64eb69aee61d4a1f7cdef9b0cce85927425d933 Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 14 May 2026 19:09:20 +0000 Subject: [PATCH] huskies: merge 1056 --- frontend/src/components/StagePanel.test.tsx | 45 +++++++++++ frontend/src/components/StagePanel.tsx | 85 +++++++++++++++++++-- 2 files changed, 122 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/StagePanel.test.tsx b/frontend/src/components/StagePanel.test.tsx index e46489cd..a0fe5a05 100644 --- a/frontend/src/components/StagePanel.test.tsx +++ b/frontend/src/components/StagePanel.test.tsx @@ -1,4 +1,5 @@ import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; import { describe, expect, it } from "vitest"; import type { PipelineStageItem } from "../api/client"; import { StagePanel } from "./StagePanel"; @@ -489,6 +490,50 @@ describe("StagePanel", () => { expect(icon).toBeInTheDocument(); expect(icon).toHaveTextContent("⏳"); }); + + it("renders gate output in a bounded box with expand and copy controls", () => { + const items: PipelineStageItem[] = [ + { + story_id: "60_story_gate_output", + name: "Gate Output Story", + error: null, + merge_failure: "Quality gates failed: cargo test output here", + agent: null, + review_hold: null, + qa: null, + depends_on: null, + }, + ]; + render(); + expect(screen.getByTestId("gate-output-text")).toHaveTextContent( + "Quality gates failed: cargo test output here", + ); + expect(screen.getByTestId("gate-output-toggle")).toBeInTheDocument(); + expect(screen.getByTestId("gate-output-copy")).toBeInTheDocument(); + }); + + it("expand toggle changes label from Expand to Collapse", async () => { + const user = userEvent.setup(); + const items: PipelineStageItem[] = [ + { + story_id: "61_story_expand", + name: "Expand Story", + error: null, + merge_failure: "A".repeat(1000), + agent: null, + review_hold: null, + qa: null, + depends_on: null, + }, + ]; + render(); + const toggle = screen.getByTestId("gate-output-toggle"); + expect(toggle).toHaveTextContent("Expand"); + await user.click(toggle); + expect(toggle).toHaveTextContent("Collapse"); + await user.click(toggle); + expect(toggle).toHaveTextContent("Expand"); + }); }); describe("StagePanel - defensive rendering", () => { diff --git a/frontend/src/components/StagePanel.tsx b/frontend/src/components/StagePanel.tsx index e3845350..5faebb43 100644 --- a/frontend/src/components/StagePanel.tsx +++ b/frontend/src/components/StagePanel.tsx @@ -5,6 +5,82 @@ import { useLozengeFly } from "./LozengeFlyContext"; const { useLayoutEffect, useRef, useState } = React; +/** Renders merge-failure gate output in a bounded scroll region with expand and copy controls. */ +function GateOutputBox({ text }: { text: string }) { + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const handleToggle = (e: React.MouseEvent) => { + e.stopPropagation(); + setExpanded((prev) => !prev); + }; + + const handleCopy = (e: React.MouseEvent) => { + e.stopPropagation(); + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 1500); + }); + }; + + const btnStyle: React.CSSProperties = { + background: "transparent", + border: "1px solid #444", + borderRadius: "4px", + color: "#aaa", + cursor: "pointer", + fontSize: "0.75em", + padding: "1px 6px", + lineHeight: 1.4, + }; + + return ( +
+
+ {text} +
+
+ + +
+
+ ); +} + type WorkItemType = "story" | "bug" | "spike" | "refactor" | "unknown"; const TYPE_COLORS: Record = { @@ -564,15 +640,8 @@ export function StagePanel({ {item.merge_failure && (
- {item.merge_failure} +
)} {item.depends_on && item.depends_on.length > 0 && (