huskies: merge 1045
This commit is contained in:
@@ -25,7 +25,7 @@ describe("StoryRow", () => {
|
|||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders name without id prefix when story_id has no leading number", () => {
|
it("renders awaiting-slot badge for merge item with no agent", () => {
|
||||||
const item: PipelineItem = {
|
const item: PipelineItem = {
|
||||||
story_id: "no-number-id",
|
story_id: "no-number-id",
|
||||||
name: "Mystery Story",
|
name: "Mystery Story",
|
||||||
@@ -33,6 +33,65 @@ describe("StoryRow", () => {
|
|||||||
};
|
};
|
||||||
const { container } = render(<StoryRow item={item} />);
|
const { container } = render(<StoryRow item={item} />);
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
|
expect(screen.getByText("awaiting-slot")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// AC1: active mergemaster is visually distinct
|
||||||
|
it("shows MERGING badge for merge item with running mergemaster (active)", () => {
|
||||||
|
const item: PipelineItem = {
|
||||||
|
story_id: "70_story_merging_active",
|
||||||
|
name: "Merging Active",
|
||||||
|
stage: "merge",
|
||||||
|
agent: { agent_name: "mergemaster", model: "claude", status: "running" },
|
||||||
|
};
|
||||||
|
render(<StoryRow item={item} />);
|
||||||
|
expect(screen.getByText("▶ MERGING")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// AC2: awaiting-slot with queue position labels
|
||||||
|
it("shows NEXT IN QUEUE for first awaiting-slot merge item", () => {
|
||||||
|
const item: PipelineItem = {
|
||||||
|
story_id: "71_story_next_in_queue",
|
||||||
|
name: "Next in Queue",
|
||||||
|
stage: "merge",
|
||||||
|
};
|
||||||
|
render(<StoryRow item={item} mergeQueuePos={1} />);
|
||||||
|
expect(screen.getByText("NEXT IN QUEUE")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows awaiting-slot with position for subsequent queued merge items", () => {
|
||||||
|
const item: PipelineItem = {
|
||||||
|
story_id: "72_story_second_in_queue",
|
||||||
|
name: "Second in Queue",
|
||||||
|
stage: "merge",
|
||||||
|
};
|
||||||
|
render(<StoryRow item={item} mergeQueuePos={2} />);
|
||||||
|
expect(screen.getByText("awaiting-slot (#2)")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// AC2: failure kind labels derived from merge_failure string
|
||||||
|
it("shows ConflictDetected for merge_failure with conflict text", () => {
|
||||||
|
const item: PipelineItem = {
|
||||||
|
story_id: "73_story_conflict",
|
||||||
|
name: "Conflict Story",
|
||||||
|
stage: "merge",
|
||||||
|
blocked: true,
|
||||||
|
merge_failure: "Merge conflict: conflicts detected",
|
||||||
|
};
|
||||||
|
render(<StoryRow item={item} />);
|
||||||
|
expect(screen.getByText("ConflictDetected")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows GatesFailed for merge_failure with quality gates text", () => {
|
||||||
|
const item: PipelineItem = {
|
||||||
|
story_id: "74_story_gates",
|
||||||
|
name: "Gates Failed Story",
|
||||||
|
stage: "merge",
|
||||||
|
blocked: true,
|
||||||
|
merge_failure: "Quality gates failed: cargo test failed",
|
||||||
|
};
|
||||||
|
render(<StoryRow item={item} />);
|
||||||
|
expect(screen.getByText("GatesFailed")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows RECOVERING badge for merge_failure item with running mergemaster", () => {
|
it("shows RECOVERING badge for merge_failure item with running mergemaster", () => {
|
||||||
|
|||||||
@@ -66,15 +66,36 @@ const STAGE_LABELS: Record<string, string> = {
|
|||||||
archived: "Archived",
|
archived: "Archived",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Derive a short label from a merge failure string based on the failure kind.
|
||||||
|
function mergeFailureKindLabel(failure: string): string {
|
||||||
|
if (failure.includes("Merge conflict") || failure.includes("CONFLICT")) {
|
||||||
|
return "ConflictDetected";
|
||||||
|
}
|
||||||
|
if (failure.includes("Quality gates failed") || failure.includes("gates failed")) {
|
||||||
|
return "GatesFailed";
|
||||||
|
}
|
||||||
|
if (failure.includes("no code changes") || failure.includes("empty diff")) {
|
||||||
|
return "EmptyDiff";
|
||||||
|
}
|
||||||
|
if (failure.includes("No commits")) {
|
||||||
|
return "NoCommits";
|
||||||
|
}
|
||||||
|
return "✕ FAILED";
|
||||||
|
}
|
||||||
|
|
||||||
/// A single story row inside a project pipeline card.
|
/// A single story row inside a project pipeline card.
|
||||||
/** Render one story row in a gateway-aggregate panel: `#<id> <name>` with stage badge. */
|
/** Render one story row in a gateway-aggregate panel: `#<id> <name>` with stage badge. */
|
||||||
export function StoryRow({ item }: { item: PipelineItem }) {
|
export function StoryRow({ item, mergeQueuePos }: { item: PipelineItem; mergeQueuePos?: number }) {
|
||||||
const isStuck = item.merge_failure != null || item.blocked;
|
const isStuck = item.merge_failure != null || item.blocked;
|
||||||
|
const isMergeActive = item.stage === "merge" && !isStuck && item.agent?.status === "running";
|
||||||
|
|
||||||
let color: string;
|
let color: string;
|
||||||
let label: string;
|
let label: string;
|
||||||
|
|
||||||
if (isStuck) {
|
if (isMergeActive) {
|
||||||
|
color = "#58a6ff";
|
||||||
|
label = "▶ MERGING";
|
||||||
|
} else if (isStuck) {
|
||||||
const agentStatus = item.agent?.status;
|
const agentStatus = item.agent?.status;
|
||||||
if (agentStatus === "running") {
|
if (agentStatus === "running") {
|
||||||
color = "#e3b341";
|
color = "#e3b341";
|
||||||
@@ -82,9 +103,24 @@ export function StoryRow({ item }: { item: PipelineItem }) {
|
|||||||
} else if (agentStatus === "pending") {
|
} else if (agentStatus === "pending") {
|
||||||
color = "#e3b341";
|
color = "#e3b341";
|
||||||
label = "⏳ QUEUED";
|
label = "⏳ QUEUED";
|
||||||
|
} else if (item.merge_failure != null) {
|
||||||
|
color = "#f85149";
|
||||||
|
label = mergeFailureKindLabel(item.merge_failure);
|
||||||
} else {
|
} else {
|
||||||
color = "#f85149";
|
color = "#f85149";
|
||||||
label = item.merge_failure != null ? "✕ FAILED" : "⊘ BLOCKED";
|
label = "⊘ BLOCKED";
|
||||||
|
}
|
||||||
|
} else if (item.stage === "merge" && item.agent?.status === "pending") {
|
||||||
|
color = "#e3b341";
|
||||||
|
label = "⏳ QUEUED";
|
||||||
|
} else if (item.stage === "merge") {
|
||||||
|
color = "#6e7681";
|
||||||
|
if (mergeQueuePos === 1) {
|
||||||
|
label = "NEXT IN QUEUE";
|
||||||
|
} else if (mergeQueuePos != null) {
|
||||||
|
label = `awaiting-slot (#${mergeQueuePos})`;
|
||||||
|
} else {
|
||||||
|
label = "awaiting-slot";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
color = STAGE_COLORS[item.stage] ?? "#8b949e";
|
color = STAGE_COLORS[item.stage] ?? "#8b949e";
|
||||||
@@ -101,6 +137,10 @@ export function StoryRow({ item }: { item: PipelineItem }) {
|
|||||||
gap: "8px",
|
gap: "8px",
|
||||||
padding: "4px 0",
|
padding: "4px 0",
|
||||||
fontSize: "0.82em",
|
fontSize: "0.82em",
|
||||||
|
background: isMergeActive ? "#58a6ff0a" : undefined,
|
||||||
|
borderRadius: isMergeActive ? "4px" : undefined,
|
||||||
|
paddingLeft: isMergeActive ? "4px" : undefined,
|
||||||
|
paddingRight: isMergeActive ? "4px" : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -446,10 +486,12 @@ function ProjectStoryRow({
|
|||||||
project,
|
project,
|
||||||
item,
|
item,
|
||||||
showProject,
|
showProject,
|
||||||
|
mergeQueuePos,
|
||||||
}: {
|
}: {
|
||||||
project: string;
|
project: string;
|
||||||
item: PipelineItem;
|
item: PipelineItem;
|
||||||
showProject: boolean;
|
showProject: boolean;
|
||||||
|
mergeQueuePos?: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
|
||||||
@@ -470,7 +512,7 @@ function ProjectStoryRow({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div style={{ flex: 1, minWidth: 0 }}>
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
<StoryRow item={item} />
|
<StoryRow item={item} mergeQueuePos={mergeQueuePos} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -503,6 +545,20 @@ function InProgressTabContent({
|
|||||||
(s) => byStage[s].length > 0,
|
(s) => byStage[s].length > 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Compute queue position among clean awaiting merge items (Stage::Merge, no failure, no running agent).
|
||||||
|
const mergeQueuePosMap = new Map<string, number>();
|
||||||
|
let queuePos = 0;
|
||||||
|
for (const { project, item } of byStage.merge) {
|
||||||
|
if (
|
||||||
|
!item.blocked &&
|
||||||
|
!item.merge_failure &&
|
||||||
|
item.agent?.status !== "running"
|
||||||
|
) {
|
||||||
|
queuePos += 1;
|
||||||
|
mergeQueuePosMap.set(`${project}:${item.story_id}`, queuePos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (allItems.length === 0) {
|
if (allItems.length === 0) {
|
||||||
return (
|
return (
|
||||||
<p style={{ color: "#6e7681", padding: "16px 0" }}>
|
<p style={{ color: "#6e7681", padding: "16px 0" }}>
|
||||||
@@ -538,6 +594,11 @@ function InProgressTabContent({
|
|||||||
project={project}
|
project={project}
|
||||||
item={item}
|
item={item}
|
||||||
showProject={multiProject}
|
showProject={multiProject}
|
||||||
|
mergeQueuePos={
|
||||||
|
stage === "merge"
|
||||||
|
? mergeQueuePosMap.get(`${project}:${item.story_id}`)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
exports[`StoryRow > renders #id prefix before the story name 1`] = `
|
exports[`StoryRow > renders #id prefix before the story name 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style="display: flex; align-items: center; gap: 8px; padding: 4px 0px; font-size: 0.82em;"
|
style="display: flex; align-items: center; gap: 8px; padding-top: 4px; padding-bottom: 4px; font-size: 0.82em;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style="padding: 1px 6px; border-radius: 10px; background: rgba(63, 185, 80, 0.133); color: rgb(63, 185, 80); border: 1px solid rgba(63, 185, 80, 0.267); white-space: nowrap; flex-shrink: 0;"
|
style="padding: 1px 6px; border-radius: 10px; background: rgba(63, 185, 80, 0.133); color: rgb(63, 185, 80); border: 1px solid rgba(63, 185, 80, 0.267); white-space: nowrap; flex-shrink: 0;"
|
||||||
@@ -29,7 +29,7 @@ exports[`StoryRow > renders #id prefix before the story name 1`] = `
|
|||||||
exports[`StoryRow > renders #id prefix for a backlogged story 1`] = `
|
exports[`StoryRow > renders #id prefix for a backlogged story 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style="display: flex; align-items: center; gap: 8px; padding: 4px 0px; font-size: 0.82em;"
|
style="display: flex; align-items: center; gap: 8px; padding-top: 4px; padding-bottom: 4px; font-size: 0.82em;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style="padding: 1px 6px; border-radius: 10px; background: rgba(210, 166, 121, 0.133); color: rgb(210, 166, 121); border: 1px solid rgba(210, 166, 121, 0.267); white-space: nowrap; flex-shrink: 0;"
|
style="padding: 1px 6px; border-radius: 10px; background: rgba(210, 166, 121, 0.133); color: rgb(210, 166, 121); border: 1px solid rgba(210, 166, 121, 0.267); white-space: nowrap; flex-shrink: 0;"
|
||||||
@@ -52,15 +52,15 @@ exports[`StoryRow > renders #id prefix for a backlogged story 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`StoryRow > renders name without id prefix when story_id has no leading number 1`] = `
|
exports[`StoryRow > renders awaiting-slot badge for merge item with no agent 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
style="display: flex; align-items: center; gap: 8px; padding: 4px 0px; font-size: 0.82em;"
|
style="display: flex; align-items: center; gap: 8px; padding-top: 4px; padding-bottom: 4px; font-size: 0.82em;"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style="padding: 1px 6px; border-radius: 10px; background: rgba(121, 192, 255, 0.133); color: rgb(121, 192, 255); border: 1px solid rgba(121, 192, 255, 0.267); white-space: nowrap; flex-shrink: 0;"
|
style="padding: 1px 6px; border-radius: 10px; background: rgba(110, 118, 129, 0.133); color: rgb(110, 118, 129); border: 1px solid rgba(110, 118, 129, 0.267); white-space: nowrap; flex-shrink: 0;"
|
||||||
>
|
>
|
||||||
Merging
|
awaiting-slot
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
style="color: rgb(230, 237, 243); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
style="color: rgb(230, 237, 243); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||||
|
|||||||
Reference in New Issue
Block a user