storkit: merge 454_story_deduplicate_work_item_display_in_web_ui_story_panel

This commit is contained in:
dave
2026-04-02 10:57:18 +00:00
parent f7f4e8f95b
commit f8d7438eec
3 changed files with 142 additions and 14 deletions
+7 -7
View File
@@ -1643,7 +1643,9 @@ describe("Slash command handling (Story 374)", () => {
}); });
it("AC: /help calls botCommand and displays response", async () => { it("AC: /help calls botCommand and displays response", async () => {
mockedApi.botCommand.mockResolvedValue({ response: "Available commands: status, help, ..." }); mockedApi.botCommand.mockResolvedValue({
response: "Available commands: status, help, ...",
});
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />); render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
await waitFor(() => expect(capturedWsHandlers).not.toBeNull()); await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
@@ -1660,11 +1662,7 @@ describe("Slash command handling (Story 374)", () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(mockedApi.botCommand).toHaveBeenCalledWith( expect(mockedApi.botCommand).toHaveBeenCalledWith("help", "", undefined);
"help",
"",
undefined,
);
}); });
expect(lastSendChatArgs).toBeNull(); expect(lastSendChatArgs).toBeNull();
}); });
@@ -1723,7 +1721,9 @@ describe("Bug 450: WebSocket error messages displayed in chat", () => {
); );
}); });
const link = await screen.findByRole("link", { name: /https:\/\/example\.com\/oauth\/login/ }); const link = await screen.findByRole("link", {
name: /https:\/\/example\.com\/oauth\/login/,
});
expect(link).toBeInTheDocument(); expect(link).toBeInTheDocument();
expect(link).toHaveAttribute("href", "https://example.com/oauth/login"); expect(link).toHaveAttribute("href", "https://example.com/oauth/login");
}); });
@@ -69,7 +69,7 @@ afterEach(() => {
}); });
describe("WorkItemDetailPanel", () => { describe("WorkItemDetailPanel", () => {
it("renders the story name in the header", async () => { it("renders the story name in the header with type and ID prefix", async () => {
render( render(
<WorkItemDetailPanel <WorkItemDetailPanel
storyId="237_bug_test" storyId="237_bug_test"
@@ -79,7 +79,7 @@ describe("WorkItemDetailPanel", () => {
); );
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent( expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
"Big Title Story", "Bug 237: Big Title Story",
); );
}); });
}); });
@@ -110,6 +110,10 @@ describe("WorkItemDetailPanel", () => {
}); });
it("renders markdown headings with constrained inline font size", async () => { it("renders markdown headings with constrained inline font size", async () => {
mockedGetWorkItemContent.mockResolvedValue({
...DEFAULT_CONTENT,
content: "# Title Heading\n\n## Section Heading\n\nSome content.",
});
render( render(
<WorkItemDetailPanel <WorkItemDetailPanel
storyId="237_bug_test" storyId="237_bug_test"
@@ -119,11 +123,95 @@ describe("WorkItemDetailPanel", () => {
); );
await waitFor(() => { await waitFor(() => {
const content = screen.getByTestId("detail-panel-content"); const content = screen.getByTestId("detail-panel-content");
const h1 = content.querySelector("h1"); // H1 is stripped by stripDisplayContent; h2 should be constrained
expect(h1).not.toBeNull(); const h2 = content.querySelector("h2");
expect(h1?.style.fontSize).toBeTruthy(); expect(h2).not.toBeNull();
expect(h2?.style.fontSize).toBeTruthy();
}); });
}); });
it("strips YAML front matter so 'name' is not shown as a prefix in content", async () => {
mockedGetWorkItemContent.mockResolvedValue({
content:
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...',
stage: "current",
name: "My Story Name",
agent: null,
});
render(
<WorkItemDetailPanel
storyId="42_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
const content = await screen.findByTestId("detail-panel-content");
expect(content.textContent).not.toMatch(/name:/i);
});
it("strips the first H1 heading so the story title is not shown twice", async () => {
mockedGetWorkItemContent.mockResolvedValue({
content:
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...',
stage: "current",
name: "My Story Name",
agent: null,
});
render(
<WorkItemDetailPanel
storyId="42_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
const content = await screen.findByTestId("detail-panel-content");
expect(content.querySelector("h1")).toBeNull();
});
it("shows 'Type N: Name' format in the panel header title (story ID/title left-justified)", async () => {
mockedGetWorkItemContent.mockResolvedValue({
content: "## User Story\n\nAs a user...",
stage: "current",
name: "My Story Name",
agent: null,
});
render(
<WorkItemDetailPanel
storyId="42_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await waitFor(() => {
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
"Story 42: My Story Name",
);
});
});
it("does not show the work item type label twice when front matter and H1 are stripped", async () => {
mockedGetWorkItemContent.mockResolvedValue({
content:
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nContent.',
stage: "current",
name: "My Story Name",
agent: null,
});
render(
<WorkItemDetailPanel
storyId="42_story_test"
pipelineVersion={0}
onClose={() => {}}
/>,
);
await screen.findByTestId("detail-panel-content");
// "Story" type label appears exactly once — in the panel header title
const title = screen.getByTestId("detail-panel-title");
expect(title.textContent).toContain("Story 42:");
// The content body should not contain an H1 repeating the type + title
const content = screen.getByTestId("detail-panel-content");
expect(content.querySelector("h1")).toBeNull();
});
}); });
describe("WorkItemDetailPanel - Agent Logs", () => { describe("WorkItemDetailPanel - Agent Logs", () => {
@@ -17,6 +17,46 @@ import { api } from "../api/client";
const { useCallback, useEffect, useRef, useState } = React; const { useCallback, useEffect, useRef, useState } = React;
/**
* Strip YAML front matter and the first H1 heading from story content before
* rendering. The panel header already shows the story ID/title, so rendering
* them again inside the markdown body creates duplicate information.
*/
function stripDisplayContent(content: string): string {
let text = content;
// Strip YAML front matter (--- ... ---)
if (text.startsWith("---")) {
const eol = text.indexOf("\n");
if (eol !== -1) {
const closeIdx = text.indexOf("\n---", eol);
if (closeIdx !== -1) {
text = text.slice(closeIdx + 4);
}
}
}
// Trim leading blank lines left by the front matter
text = text.trimStart();
// Strip the first H1 heading — it duplicates the panel header title
if (text.startsWith("# ")) {
const eol = text.indexOf("\n");
text = eol !== -1 ? text.slice(eol + 1).trimStart() : "";
}
return text;
}
/**
* Format the story ID/title line shown in the panel header.
* Produces e.g. "Story 454: My Story Name" or "Bug 12: Crash on startup".
* Falls back to name or storyId when the pattern doesn't match.
*/
function formatStoryTitle(storyId: string, name: string | null): string {
const match = storyId.match(/^(\d+)_([a-z]+)_/);
if (!match || !name) return name ?? storyId;
const [, number, type] = match;
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
return `${typeLabel} ${number}: ${name}`;
}
const STAGE_LABELS: Record<string, string> = { const STAGE_LABELS: Record<string, string> = {
backlog: "Backlog", backlog: "Backlog",
current: "Current", current: "Current",
@@ -352,7 +392,7 @@ export function WorkItemDetailPanel({
whiteSpace: "nowrap", whiteSpace: "nowrap",
}} }}
> >
{name ?? storyId} {formatStoryTitle(storyId, name)}
</div> </div>
{stage && ( {stage && (
<div <div
@@ -504,7 +544,7 @@ export function WorkItemDetailPanel({
), ),
}} }}
> >
{content} {stripDisplayContent(content)}
</Markdown> </Markdown>
</div> </div>
)} )}