From e6d051d01615801245d3f9b3dc4fff3620e7fb29 Mon Sep 17 00:00:00 2001 From: dave Date: Wed, 13 May 2026 15:25:02 +0000 Subject: [PATCH] huskies: merge 983 --- frontend/src/components/GatewayPanel.test.tsx | 72 ++++++- frontend/src/components/GatewayPanel.tsx | 86 +++------ frontend/src/components/StagePanel.test.tsx | 98 ++++++++++ frontend/src/components/StagePanel.tsx | 179 +++++++++--------- 4 files changed, 283 insertions(+), 152 deletions(-) diff --git a/frontend/src/components/GatewayPanel.test.tsx b/frontend/src/components/GatewayPanel.test.tsx index 1b0dd2b7..561ed49a 100644 --- a/frontend/src/components/GatewayPanel.test.tsx +++ b/frontend/src/components/GatewayPanel.test.tsx @@ -1,5 +1,5 @@ /** Tests for GatewayPanel — verifies story id and name rendering in the gateway aggregate view. */ -import { render } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { describe, expect, it } from "vitest"; import type { PipelineItem } from "../api/gateway"; import { StoryRow } from "./GatewayPanel"; @@ -34,4 +34,74 @@ describe("StoryRow", () => { const { container } = render(); expect(container).toMatchSnapshot(); }); + + it("shows RECOVERING badge for merge_failure item with running mergemaster", () => { + const item: PipelineItem = { + story_id: "60_story_merge_recovering", + name: "Merge Recovering", + stage: "merge", + merge_failure: "Squash merge failed", + agent: { agent_name: "mergemaster", model: "claude", status: "running" }, + }; + render(); + expect(screen.getByText("⟳ RECOVERING")).toBeInTheDocument(); + }); + + it("shows QUEUED badge for merge_failure item with pending mergemaster", () => { + const item: PipelineItem = { + story_id: "61_story_merge_queued", + name: "Merge Queued", + stage: "merge", + merge_failure: "Squash merge failed", + agent: { agent_name: "mergemaster", model: "claude", status: "pending" }, + }; + render(); + expect(screen.getByText("⏳ QUEUED")).toBeInTheDocument(); + }); + + it("shows FAILED badge for merge_failure item with no recovery agent", () => { + const item: PipelineItem = { + story_id: "62_story_merge_final", + name: "Merge Final", + stage: "merge", + merge_failure: "Squash merge failed", + }; + render(); + expect(screen.getByText("✕ FAILED")).toBeInTheDocument(); + }); + + it("shows RECOVERING badge for blocked item with running recovery agent", () => { + const item: PipelineItem = { + story_id: "63_story_blocked_recovering", + name: "Blocked Recovering", + stage: "current", + blocked: true, + agent: { agent_name: "coder", model: "claude", status: "running" }, + }; + render(); + expect(screen.getByText("⟳ RECOVERING")).toBeInTheDocument(); + }); + + it("shows QUEUED badge for blocked item with pending recovery agent", () => { + const item: PipelineItem = { + story_id: "64_story_blocked_queued", + name: "Blocked Queued", + stage: "current", + blocked: true, + agent: { agent_name: "coder", model: "claude", status: "pending" }, + }; + render(); + expect(screen.getByText("⏳ QUEUED")).toBeInTheDocument(); + }); + + it("shows BLOCKED badge for blocked item with no recovery agent", () => { + const item: PipelineItem = { + story_id: "65_story_blocked_human", + name: "Blocked Human", + stage: "current", + blocked: true, + }; + render(); + expect(screen.getByText("⊘ BLOCKED")).toBeInTheDocument(); + }); }); diff --git a/frontend/src/components/GatewayPanel.tsx b/frontend/src/components/GatewayPanel.tsx index 9277721b..1c9a6af6 100644 --- a/frontend/src/components/GatewayPanel.tsx +++ b/frontend/src/components/GatewayPanel.tsx @@ -65,70 +65,29 @@ const STAGE_LABELS: Record = { /// A single story row inside a project pipeline card. /** Render one story row in a gateway-aggregate panel: `# ` with stage badge. */ export function StoryRow({ item }: { item: PipelineItem }) { - const color = STAGE_COLORS[item.stage] ?? "#8b949e"; - const label = STAGE_LABELS[item.stage] ?? item.stage; - const idNum = item.story_id.match(/^(\d+)/)?.[1]; - const agentStatus = item.agent?.status; - const isStuck = item.blocked || (item.merge_failure != null && item.merge_failure !== ""); + const isStuck = item.merge_failure != null || item.blocked; - const recoveryBadge = isStuck - ? agentStatus === "running" - ? ( - - ⟳ RECOVERING - - ) - : agentStatus === "pending" - ? ( - - ⏳ QUEUED - - ) - : ( - - ⊘ STUCK - - ) - : null; + let color: string; + let label: string; + + if (isStuck) { + const agentStatus = item.agent?.status; + if (agentStatus === "running") { + color = "#e3b341"; + label = "⟳ RECOVERING"; + } else if (agentStatus === "pending") { + color = "#e3b341"; + label = "⏳ QUEUED"; + } else { + color = "#f85149"; + label = item.merge_failure != null ? "✕ FAILED" : "⊘ BLOCKED"; + } + } else { + color = STAGE_COLORS[item.stage] ?? "#8b949e"; + label = STAGE_LABELS[item.stage] ?? item.stage; + } + + const idNum = item.story_id.match(/^(\d+)/)?.[1]; return (
{label} - {recoveryBadge} {idNum && #{idNum}{" "}} {item.name} diff --git a/frontend/src/components/StagePanel.test.tsx b/frontend/src/components/StagePanel.test.tsx index b6f4900a..9ae34929 100644 --- a/frontend/src/components/StagePanel.test.tsx +++ b/frontend/src/components/StagePanel.test.tsx @@ -391,4 +391,102 @@ describe("StagePanel", () => { screen.queryByTestId("merge-in-flight-icon-42_story_no_prop"), ).not.toBeInTheDocument(); }); + + it("shows spinning RECOVERING badge for blocked item with running recovery agent", () => { + const items: PipelineStageItem[] = [ + { + story_id: "50_story_blocked_recovering", + name: "Blocked Recovering Story", + error: null, + merge_failure: null, + agent: { agent_name: "coder", model: "claude", status: "running" }, + review_hold: null, + qa: null, + depends_on: null, + blocked: true, + }, + ]; + render(); + const badge = screen.getByTestId("blocked-badge-50_story_blocked_recovering"); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent("RECOVERING"); + }); + + it("shows QUEUED badge for blocked item with pending recovery agent", () => { + const items: PipelineStageItem[] = [ + { + story_id: "51_story_blocked_queued", + name: "Blocked Queued Story", + error: null, + merge_failure: null, + agent: { agent_name: "coder", model: "claude", status: "pending" }, + review_hold: null, + qa: null, + depends_on: null, + blocked: true, + }, + ]; + render(); + const badge = screen.getByTestId("blocked-badge-51_story_blocked_queued"); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent("QUEUED"); + }); + + it("shows red BLOCKED badge for blocked item with no recovery agent", () => { + const items: PipelineStageItem[] = [ + { + story_id: "52_story_blocked_human", + name: "Blocked Human Story", + error: null, + merge_failure: null, + agent: null, + review_hold: null, + qa: null, + depends_on: null, + blocked: true, + }, + ]; + render(); + const badge = screen.getByTestId("blocked-badge-52_story_blocked_human"); + expect(badge).toBeInTheDocument(); + expect(badge).toHaveTextContent("BLOCKED"); + }); + + it("shows spinning icon for merge_failure item with running mergemaster", () => { + const items: PipelineStageItem[] = [ + { + story_id: "53_story_merge_recovering", + name: "Merge Recovering Story", + error: null, + merge_failure: "Squash merge failed: conflicts", + agent: { agent_name: "mergemaster", model: "claude", status: "running" }, + review_hold: null, + qa: null, + depends_on: null, + }, + ]; + render(); + const icon = screen.getByTestId("merge-failure-icon-53_story_merge_recovering"); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveTextContent("⟳"); + }); + + it("shows hourglass icon for merge_failure item with pending mergemaster", () => { + const items: PipelineStageItem[] = [ + { + story_id: "54_story_merge_queued", + name: "Merge Queued Story", + error: null, + merge_failure: "Squash merge failed: conflicts", + agent: { agent_name: "mergemaster", model: "claude", status: "pending" }, + review_hold: null, + qa: null, + depends_on: null, + }, + ]; + render(); + const icon = screen.getByTestId("merge-failure-icon-54_story_merge_queued"); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveTextContent("⏳"); + }); }); diff --git a/frontend/src/components/StagePanel.tsx b/frontend/src/components/StagePanel.tsx index f384944c..3c5b2e9f 100644 --- a/frontend/src/components/StagePanel.tsx +++ b/frontend/src/components/StagePanel.tsx @@ -345,53 +345,54 @@ export function StagePanel({ <>
- {hasMergeFailure && (() => { - const agentStatus = item.agent?.status; - if (agentStatus === "running") { + {hasMergeFailure && + (() => { + const agentStatus = item.agent?.status; + if (agentStatus === "running") { + return ( + + ⟳ + + ); + } + if (agentStatus === "pending") { + return ( + + ⏳ + + ); + } return ( - ⟳ - - ); - } - if (agentStatus === "pending") { - return ( - - ⏳ + ✕ ); - } - return ( - - ✕ - - ); - })()} + })()} {mergesInFlight?.has(item.story_id) && ( )} - {item.blocked && !item.merge_failure && (() => { - const agentStatus = item.agent?.status; - if (agentStatus === "running") { + {item.blocked && + !item.merge_failure && + (() => { + const agentStatus = item.agent?.status; + if (agentStatus === "running") { + return ( + + ⟳ RECOVERING + + ); + } + if (agentStatus === "pending") { + return ( + + ⏳ QUEUED + + ); + } return ( - ⟳ RECOVERING + ⊘ BLOCKED ); - } - if (agentStatus === "pending") { - return ( - - ⏳ QUEUED - - ); - } - return ( - - ⊘ BLOCKED - - ); - })()} + })()} {item.frozen && (