huskies: merge 983

This commit is contained in:
dave
2026-05-13 15:25:02 +00:00
parent f268dca5bb
commit e6d051d016
4 changed files with 283 additions and 152 deletions
+71 -1
View File
@@ -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(<StoryRow item={item} />);
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(<StoryRow item={item} />);
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(<StoryRow item={item} />);
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(<StoryRow item={item} />);
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(<StoryRow item={item} />);
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(<StoryRow item={item} />);
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(<StoryRow item={item} />);
expect(screen.getByText("⊘ BLOCKED")).toBeInTheDocument();
});
});
+22 -64
View File
@@ -65,70 +65,29 @@ const STAGE_LABELS: Record<string, string> = {
/// A single story row inside a project pipeline card.
/** Render one story row in a gateway-aggregate panel: `#<id> <name>` 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"
? (
<span
data-testid={`recovery-badge-${item.story_id}`}
title="Recovery in progress — no human action needed"
style={{
fontSize: "0.75em",
fontWeight: 700,
color: "#e3b341",
background: "#2a1e00",
border: "1px solid #6e4f00",
borderRadius: "4px",
padding: "1px 5px",
flexShrink: 0,
}}
>
RECOVERING
</span>
)
: agentStatus === "pending"
? (
<span
data-testid={`recovery-badge-${item.story_id}`}
title="Recovery scheduled — waiting for a slot"
style={{
fontSize: "0.75em",
fontWeight: 700,
color: "#e3b341",
background: "#2a1e00",
border: "1px solid #6e4f00",
borderRadius: "4px",
padding: "1px 5px",
flexShrink: 0,
}}
>
QUEUED
</span>
)
: (
<span
data-testid={`recovery-badge-${item.story_id}`}
title={item.merge_failure ? "Merge failed — needs human" : "Blocked — awaiting human unblock"}
style={{
fontSize: "0.75em",
fontWeight: 700,
color: "#f85149",
background: "#2a1010",
border: "1px solid #6e1b1b",
borderRadius: "4px",
padding: "1px 5px",
flexShrink: 0,
}}
>
STUCK
</span>
)
: 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 (
<div
@@ -153,7 +112,6 @@ export function StoryRow({ item }: { item: PipelineItem }) {
>
{label}
</span>
{recoveryBadge}
<span style={{ color: "#e6edf3", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{idNum && <span style={{ color: "#8b949e", fontFamily: "monospace" }}>#{idNum}{" "}</span>}
{item.name}
@@ -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(<StagePanel title="Current" items={items} />);
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(<StagePanel title="Current" items={items} />);
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(<StagePanel title="Current" items={items} />);
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(<StagePanel title="Merge" items={items} />);
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(<StagePanel title="Merge" items={items} />);
const icon = screen.getByTestId("merge-failure-icon-54_story_merge_queued");
expect(icon).toBeInTheDocument();
expect(icon).toHaveTextContent("⏳");
});
});
+13 -8
View File
@@ -345,7 +345,8 @@ export function StagePanel({
<>
<div style={{ flex: 1 }}>
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
{hasMergeFailure && (() => {
{hasMergeFailure &&
(() => {
const agentStatus = item.agent?.status;
if (agentStatus === "running") {
return (
@@ -430,23 +431,27 @@ export function StagePanel({
{typeLabel}
</span>
)}
{item.blocked && !item.merge_failure && (() => {
{item.blocked &&
!item.merge_failure &&
(() => {
const agentStatus = item.agent?.status;
if (agentStatus === "running") {
return (
<span
data-testid={`blocked-badge-${item.story_id}`}
title="Recovery in progress — no human action needed"
title="Recovery coder running — no human action needed"
style={{
display: "inline-block",
fontSize: "0.65em",
fontWeight: 700,
color: "#e3b341",
background: "#2a1e00",
border: "1px solid #6e4f00",
background: "#2a1f0a",
border: "1px solid #6e4a00",
borderRadius: "4px",
padding: "1px 4px",
marginRight: "8px",
letterSpacing: "0.05em",
animation: "spin 1s linear infinite",
}}
>
RECOVERING
@@ -457,13 +462,13 @@ export function StagePanel({
return (
<span
data-testid={`blocked-badge-${item.story_id}`}
title="Recovery scheduled — waiting for a slot"
title="Recovery coder queued — waiting for a slot"
style={{
fontSize: "0.65em",
fontWeight: 700,
color: "#e3b341",
background: "#2a1e00",
border: "1px solid #6e4f00",
background: "#2a1f0a",
border: "1px solid #6e4a00",
borderRadius: "4px",
padding: "1px 4px",
marginRight: "8px",