huskies: merge 487_story_display_story_dependencies_in_web_ui_and_chat_commands
This commit is contained in:
+21
@@ -0,0 +1,21 @@
|
|||||||
|
---
|
||||||
|
name: "Display story dependencies in web UI and chat commands"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 487: Display story dependencies in web UI and chat commands
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a user managing stories with dependencies, I want to see dependency information in the web UI story panel and in chat/slash command output, so I can understand which stories are blocked and why without reading the raw markdown files.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] The `UpcomingStory` API response includes a `depends_on` field (list of story numbers) when the story has dependencies in its front matter
|
||||||
|
- [ ] The `status <number>` chat command shows `depends_on` in the front matter fields section when present
|
||||||
|
- [ ] The web UI pipeline stage cards show a "Depends on: #N, #M" badge when a story has dependencies
|
||||||
|
- [ ] Stories with no `depends_on` field show no dependency indicator (no regressions)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- Rendering the dependency status (done/not done) in the UI — only the raw list of numbers is shown
|
||||||
|
- Adding or removing dependencies via the web UI (the existing `depends` chat command handles that)
|
||||||
@@ -48,6 +48,7 @@ export interface PipelineStageItem {
|
|||||||
agent: AgentAssignment | null;
|
agent: AgentAssignment | null;
|
||||||
review_hold: boolean | null;
|
review_hold: boolean | null;
|
||||||
qa: string | null;
|
qa: string | null;
|
||||||
|
depends_on: number[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PipelineState {
|
export interface PipelineState {
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ describe("AgentLozenge fixed intrinsic width", () => {
|
|||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const pipeline = makePipeline({ current: items });
|
const pipeline = makePipeline({ current: items });
|
||||||
@@ -115,6 +116,7 @@ describe("LozengeFlyProvider fly-in visibility", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -157,6 +159,7 @@ describe("LozengeFlyProvider fly-in visibility", () => {
|
|||||||
},
|
},
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -221,6 +224,7 @@ describe("LozengeFlyProvider fly-in clone", () => {
|
|||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -264,6 +268,7 @@ describe("LozengeFlyProvider fly-in clone", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -313,6 +318,7 @@ describe("LozengeFlyProvider fly-in clone", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -384,6 +390,7 @@ describe("LozengeFlyProvider fly-out", () => {
|
|||||||
agent: { agent_name: "coder-1", model: "haiku", status: "completed" },
|
agent: { agent_name: "coder-1", model: "haiku", status: "completed" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -411,6 +418,7 @@ describe("LozengeFlyProvider fly-out", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -445,6 +453,7 @@ describe("AgentLozenge idle vs active appearance", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
@@ -471,6 +480,7 @@ describe("AgentLozenge idle vs active appearance", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "pending" },
|
agent: { agent_name: "coder-1", model: null, status: "pending" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
@@ -497,6 +507,7 @@ describe("AgentLozenge idle vs active appearance", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const { container } = render(
|
const { container } = render(
|
||||||
@@ -550,6 +561,7 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -573,6 +585,7 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -597,6 +610,7 @@ describe("hiddenRosterAgents: assigned agents are absent from roster", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -659,6 +673,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -672,6 +687,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -716,6 +732,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
agent: { agent_name: "coder-1", model: null, status: "completed" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -729,6 +746,7 @@ describe("hiddenRosterAgents: fly-out keeps agent hidden until clone lands", ()
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -804,6 +822,7 @@ describe("LozengeFlyProvider agent swap (name change)", () => {
|
|||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -817,6 +836,7 @@ describe("LozengeFlyProvider agent swap (name change)", () => {
|
|||||||
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -903,6 +923,7 @@ describe("LozengeFlyProvider fly-out without roster element", () => {
|
|||||||
},
|
},
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -916,6 +937,7 @@ describe("LozengeFlyProvider fly-out without roster element", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -989,6 +1011,7 @@ describe("FlyingLozengeClone initial non-flying render", () => {
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1066,6 +1089,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
agent: { agent_name: "coder-1", model: "sonnet", status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1079,6 +1103,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
agent: { agent_name: "coder-2", model: "haiku", status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1147,6 +1172,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
agent: { agent_name: "coder-1", model: null, status: "running" },
|
agent: { agent_name: "coder-1", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1160,6 +1186,7 @@ describe("Bug 137: no animation actions lost during rapid pipeline updates", ()
|
|||||||
agent: { agent_name: "coder-2", model: null, status: "running" },
|
agent: { agent_name: "coder-2", model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -1247,6 +1274,7 @@ describe("Bug 137: animations remain functional through sustained agent activity
|
|||||||
agent: { agent_name: agentName, model: null, status: "running" },
|
agent: { agent_name: agentName, model: null, status: "running" },
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Current" items={items} />);
|
render(<StagePanel title="Current" items={items} />);
|
||||||
@@ -41,6 +42,7 @@ describe("StagePanel", () => {
|
|||||||
},
|
},
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Current" items={items} />);
|
render(<StagePanel title="Current" items={items} />);
|
||||||
@@ -62,6 +64,7 @@ describe("StagePanel", () => {
|
|||||||
},
|
},
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Current" items={items} />);
|
render(<StagePanel title="Current" items={items} />);
|
||||||
@@ -82,6 +85,7 @@ describe("StagePanel", () => {
|
|||||||
},
|
},
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="QA" items={items} />);
|
render(<StagePanel title="QA" items={items} />);
|
||||||
@@ -98,6 +102,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Current" items={items} />);
|
render(<StagePanel title="Current" items={items} />);
|
||||||
@@ -114,6 +119,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Upcoming" items={items} />);
|
render(<StagePanel title="Upcoming" items={items} />);
|
||||||
@@ -130,6 +136,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Upcoming" items={items} />);
|
render(<StagePanel title="Upcoming" items={items} />);
|
||||||
@@ -148,6 +155,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Current" items={items} />);
|
render(<StagePanel title="Current" items={items} />);
|
||||||
@@ -166,6 +174,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="QA" items={items} />);
|
render(<StagePanel title="QA" items={items} />);
|
||||||
@@ -184,6 +193,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Done" items={items} />);
|
render(<StagePanel title="Done" items={items} />);
|
||||||
@@ -202,6 +212,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Upcoming" items={items} />);
|
render(<StagePanel title="Upcoming" items={items} />);
|
||||||
@@ -223,6 +234,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Current" items={items} />);
|
render(<StagePanel title="Current" items={items} />);
|
||||||
@@ -241,6 +253,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="QA" items={items} />);
|
render(<StagePanel title="QA" items={items} />);
|
||||||
@@ -259,6 +272,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Done" items={items} />);
|
render(<StagePanel title="Done" items={items} />);
|
||||||
@@ -277,6 +291,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Merge" items={items} />);
|
render(<StagePanel title="Merge" items={items} />);
|
||||||
@@ -298,6 +313,7 @@ describe("StagePanel", () => {
|
|||||||
agent: null,
|
agent: null,
|
||||||
review_hold: null,
|
review_hold: null,
|
||||||
qa: null,
|
qa: null,
|
||||||
|
depends_on: null,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
render(<StagePanel title="Merge" items={items} />);
|
render(<StagePanel title="Merge" items={items} />);
|
||||||
|
|||||||
@@ -420,6 +420,19 @@ export function StagePanel({
|
|||||||
{item.merge_failure}
|
{item.merge_failure}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{item.depends_on && item.depends_on.length > 0 && (
|
||||||
|
<div
|
||||||
|
data-testid={`depends-on-${item.story_id}`}
|
||||||
|
style={{
|
||||||
|
fontSize: "0.75em",
|
||||||
|
color: "#888",
|
||||||
|
marginTop: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Depends on:{" "}
|
||||||
|
{item.depends_on.map((n) => `#${n}`).join(", ")}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{item.agent && (
|
{item.agent && (
|
||||||
<AgentLozenge
|
<AgentLozenge
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ fn build_triage_dump(
|
|||||||
if let Some(ref mf) = m.merge_failure {
|
if let Some(ref mf) = m.merge_failure {
|
||||||
fields.push(format!("**merge_failure:** {mf}"));
|
fields.push(format!("**merge_failure:** {mf}"));
|
||||||
}
|
}
|
||||||
|
if let Some(ref deps) = m.depends_on
|
||||||
|
&& !deps.is_empty()
|
||||||
|
{
|
||||||
|
let nums: Vec<String> = deps.iter().map(|n| format!("#{n}")).collect();
|
||||||
|
fields.push(format!("**depends_on:** {}", nums.join(", ")));
|
||||||
|
}
|
||||||
if !fields.is_empty() {
|
if !fields.is_empty() {
|
||||||
out.push_str("**Front matter:**\n");
|
out.push_str("**Front matter:**\n");
|
||||||
for f in &fields {
|
for f in &fields {
|
||||||
@@ -445,6 +451,23 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn whatsup_shows_depends_on_field() {
|
||||||
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
write_story_file(
|
||||||
|
tmp.path(),
|
||||||
|
"2_current",
|
||||||
|
"55_story_depends_story.md",
|
||||||
|
"---\nname: Depends Story\ndepends_on: [477, 478]\n---\n",
|
||||||
|
);
|
||||||
|
let output = status_triage_cmd(tmp.path(), "55").unwrap();
|
||||||
|
assert!(
|
||||||
|
output.contains("depends_on") || output.contains("#477"),
|
||||||
|
"should show depends_on field: {output}"
|
||||||
|
);
|
||||||
|
assert!(output.contains("478"), "should list all dependency numbers: {output}");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn whatsup_no_worktree_shows_not_created() {
|
fn whatsup_no_worktree_shows_not_created() {
|
||||||
let tmp = tempfile::TempDir::new().unwrap();
|
let tmp = tempfile::TempDir::new().unwrap();
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ pub struct UpcomingStory {
|
|||||||
/// True when the story has exceeded its retry limit and will not be auto-assigned.
|
/// True when the story has exceeded its retry limit and will not be auto-assigned.
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub blocked: Option<bool>,
|
pub blocked: Option<bool>,
|
||||||
|
/// Story numbers this story depends on.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub depends_on: Option<Vec<u32>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StoryValidationResult {
|
pub struct StoryValidationResult {
|
||||||
@@ -143,12 +146,12 @@ fn load_stage_items(
|
|||||||
.to_string();
|
.to_string();
|
||||||
let contents = fs::read_to_string(&path)
|
let contents = fs::read_to_string(&path)
|
||||||
.map_err(|e| format!("Failed to read story file {}: {e}", path.display()))?;
|
.map_err(|e| format!("Failed to read story file {}: {e}", path.display()))?;
|
||||||
let (name, error, merge_failure, review_hold, qa, retry_count, blocked) = match parse_front_matter(&contents) {
|
let (name, error, merge_failure, review_hold, qa, retry_count, blocked, depends_on) = match parse_front_matter(&contents) {
|
||||||
Ok(meta) => (meta.name, None, meta.merge_failure, meta.review_hold, meta.qa.map(|m| m.as_str().to_string()), meta.retry_count, meta.blocked),
|
Ok(meta) => (meta.name, None, meta.merge_failure, meta.review_hold, meta.qa.map(|m| m.as_str().to_string()), meta.retry_count, meta.blocked, meta.depends_on),
|
||||||
Err(e) => (None, Some(e.to_string()), None, None, None, None, None),
|
Err(e) => (None, Some(e.to_string()), None, None, None, None, None, None),
|
||||||
};
|
};
|
||||||
let agent = agent_map.get(&story_id).cloned();
|
let agent = agent_map.get(&story_id).cloned();
|
||||||
stories.push(UpcomingStory { story_id, name, error, merge_failure, agent, review_hold, qa, retry_count, blocked });
|
stories.push(UpcomingStory { story_id, name, error, merge_failure, agent, review_hold, qa, retry_count, blocked, depends_on });
|
||||||
}
|
}
|
||||||
|
|
||||||
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
stories.sort_by(|a, b| a.story_id.cmp(&b.story_id));
|
||||||
@@ -526,6 +529,32 @@ mod tests {
|
|||||||
assert_eq!(item.agent.as_ref().unwrap().status, "pending");
|
assert_eq!(item.agent.as_ref().unwrap().status, "pending");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pipeline_state_includes_depends_on() {
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let backlog = tmp.path().join(".huskies/work/1_backlog");
|
||||||
|
fs::create_dir_all(&backlog).unwrap();
|
||||||
|
fs::write(
|
||||||
|
backlog.join("20_story_dependent.md"),
|
||||||
|
"---\nname: Dependent Story\ndepends_on: [10, 11]\n---\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(
|
||||||
|
backlog.join("21_story_independent.md"),
|
||||||
|
"---\nname: Independent Story\n---\n",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let ctx = crate::http::context::AppContext::new_test(tmp.path().to_path_buf());
|
||||||
|
let state = load_pipeline_state(&ctx).unwrap();
|
||||||
|
|
||||||
|
let dependent = state.backlog.iter().find(|s| s.story_id == "20_story_dependent").unwrap();
|
||||||
|
assert_eq!(dependent.depends_on, Some(vec![10, 11]));
|
||||||
|
|
||||||
|
let independent = state.backlog.iter().find(|s| s.story_id == "21_story_independent").unwrap();
|
||||||
|
assert_eq!(independent.depends_on, None);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn load_upcoming_parses_metadata() {
|
fn load_upcoming_parses_metadata() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
|||||||
@@ -795,6 +795,7 @@ mod tests {
|
|||||||
qa: None,
|
qa: None,
|
||||||
retry_count: None,
|
retry_count: None,
|
||||||
blocked: None,
|
blocked: None,
|
||||||
|
depends_on: None,
|
||||||
};
|
};
|
||||||
let resp = WsResponse::PipelineState {
|
let resp = WsResponse::PipelineState {
|
||||||
backlog: vec![story],
|
backlog: vec![story],
|
||||||
@@ -937,6 +938,7 @@ mod tests {
|
|||||||
qa: None,
|
qa: None,
|
||||||
retry_count: None,
|
retry_count: None,
|
||||||
blocked: None,
|
blocked: None,
|
||||||
|
depends_on: None,
|
||||||
}],
|
}],
|
||||||
current: vec![UpcomingStory {
|
current: vec![UpcomingStory {
|
||||||
story_id: "2_story_b".to_string(),
|
story_id: "2_story_b".to_string(),
|
||||||
@@ -948,6 +950,7 @@ mod tests {
|
|||||||
qa: None,
|
qa: None,
|
||||||
retry_count: None,
|
retry_count: None,
|
||||||
blocked: None,
|
blocked: None,
|
||||||
|
depends_on: None,
|
||||||
}],
|
}],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
@@ -961,6 +964,7 @@ mod tests {
|
|||||||
qa: None,
|
qa: None,
|
||||||
retry_count: None,
|
retry_count: None,
|
||||||
blocked: None,
|
blocked: None,
|
||||||
|
depends_on: None,
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
let resp: WsResponse = state.into();
|
let resp: WsResponse = state.into();
|
||||||
@@ -1121,6 +1125,7 @@ mod tests {
|
|||||||
qa: None,
|
qa: None,
|
||||||
retry_count: None,
|
retry_count: None,
|
||||||
blocked: None,
|
blocked: None,
|
||||||
|
depends_on: None,
|
||||||
}],
|
}],
|
||||||
qa: vec![],
|
qa: vec![],
|
||||||
merge: vec![],
|
merge: vec![],
|
||||||
|
|||||||
Reference in New Issue
Block a user