Finishing agent merge
This commit is contained in:
@@ -475,8 +475,8 @@ describe("Chat review panel", () => {
|
||||
it("fetches upcoming stories on mount and renders panel", async () => {
|
||||
mockedWorkflow.getUpcomingStories.mockResolvedValueOnce({
|
||||
stories: [
|
||||
{ story_id: "31_view_upcoming", name: "View Upcoming Stories" },
|
||||
{ story_id: "32_worktree", name: null },
|
||||
{ story_id: "31_view_upcoming", name: "View Upcoming Stories", error: null },
|
||||
{ story_id: "32_worktree", name: null, error: null },
|
||||
],
|
||||
});
|
||||
|
||||
@@ -544,6 +544,7 @@ describe("Chat review panel", () => {
|
||||
"The UI lists unchecked acceptance criteria.",
|
||||
"Each TODO is displayed as its full text.",
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -566,6 +567,7 @@ describe("Chat review panel", () => {
|
||||
story_id: "28_ui_show_test_todos",
|
||||
story_name: "Show Remaining Test TODOs in the UI",
|
||||
todos: [],
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -65,7 +65,12 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
const [isCollectingCoverage, setIsCollectingCoverage] = useState(false);
|
||||
const [coverageError, setCoverageError] = useState<string | null>(null);
|
||||
const [storyTodos, setStoryTodos] = useState<
|
||||
{ storyId: string; storyName: string | null; items: string[] }[]
|
||||
{
|
||||
storyId: string;
|
||||
storyName: string | null;
|
||||
items: string[];
|
||||
error: string | null;
|
||||
}[]
|
||||
>([]);
|
||||
const [todoError, setTodoError] = useState<string | null>(null);
|
||||
const [isTodoLoading, setIsTodoLoading] = useState(false);
|
||||
@@ -284,6 +289,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
storyId: s.story_id,
|
||||
storyName: s.story_name,
|
||||
items: s.todos,
|
||||
error: s.error ?? null,
|
||||
})),
|
||||
);
|
||||
setLastTodoRefresh(new Date());
|
||||
@@ -319,6 +325,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
||||
storyId: s.story_id,
|
||||
storyName: s.story_name,
|
||||
items: s.todos,
|
||||
error: s.error ?? null,
|
||||
})),
|
||||
);
|
||||
setLastTodoRefresh(new Date());
|
||||
|
||||
76
frontend/src/components/TodoPanel.test.tsx
Normal file
76
frontend/src/components/TodoPanel.test.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { TodoPanel } from "./TodoPanel";
|
||||
|
||||
const baseProps = {
|
||||
todos: [] as {
|
||||
storyId: string;
|
||||
storyName: string | null;
|
||||
items: string[];
|
||||
error: string | null;
|
||||
}[],
|
||||
isTodoLoading: false,
|
||||
todoError: null,
|
||||
lastTodoRefresh: null,
|
||||
onRefresh: vi.fn(),
|
||||
};
|
||||
|
||||
describe("TodoPanel", () => {
|
||||
it("shows per-story front matter error", () => {
|
||||
render(
|
||||
<TodoPanel
|
||||
{...baseProps}
|
||||
todos={[
|
||||
{
|
||||
storyId: "28_todos",
|
||||
storyName: null,
|
||||
items: [],
|
||||
error: "Missing front matter",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Missing front matter")).toBeInTheDocument();
|
||||
expect(screen.getByText("28_todos")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows error alongside todo items", () => {
|
||||
render(
|
||||
<TodoPanel
|
||||
{...baseProps}
|
||||
todos={[
|
||||
{
|
||||
storyId: "28_todos",
|
||||
storyName: "Show TODOs",
|
||||
items: ["First criterion"],
|
||||
error: "Missing 'test_plan' field",
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Missing 'test_plan' field")).toBeInTheDocument();
|
||||
expect(screen.getByText("First criterion")).toBeInTheDocument();
|
||||
expect(screen.getByText("Show TODOs")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not show error when null", () => {
|
||||
render(
|
||||
<TodoPanel
|
||||
{...baseProps}
|
||||
todos={[
|
||||
{
|
||||
storyId: "28_todos",
|
||||
storyName: "Show TODOs",
|
||||
items: ["A criterion"],
|
||||
error: null,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByTestId("story-error-28_todos")).toBeNull();
|
||||
expect(screen.getByText("A criterion")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ interface StoryTodos {
|
||||
storyId: string;
|
||||
storyName: string | null;
|
||||
items: string[];
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface TodoPanelProps {
|
||||
@@ -29,6 +30,7 @@ export function TodoPanel({
|
||||
onRefresh,
|
||||
}: TodoPanelProps) {
|
||||
const totalTodos = todos.reduce((sum, s) => sum + s.items.length, 0);
|
||||
const hasErrors = todos.some((s) => s.error);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -127,7 +129,7 @@ export function TodoPanel({
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
) : totalTodos === 0 ? (
|
||||
) : totalTodos === 0 && !hasErrors ? (
|
||||
<div style={{ fontSize: "0.85em", color: "#7ee787" }}>
|
||||
All acceptance criteria complete.
|
||||
</div>
|
||||
@@ -140,7 +142,7 @@ export function TodoPanel({
|
||||
}}
|
||||
>
|
||||
{todos
|
||||
.filter((s) => s.items.length > 0)
|
||||
.filter((s) => s.items.length > 0 || s.error)
|
||||
.map((story) => (
|
||||
<div key={story.storyId}>
|
||||
<div
|
||||
@@ -152,18 +154,32 @@ export function TodoPanel({
|
||||
>
|
||||
{story.storyName ?? story.storyId}
|
||||
</div>
|
||||
<ul
|
||||
style={{
|
||||
margin: "0 0 0 16px",
|
||||
padding: 0,
|
||||
fontSize: "0.85em",
|
||||
color: "#ccc",
|
||||
}}
|
||||
>
|
||||
{story.items.map((item) => (
|
||||
<li key={`todo-${story.storyId}-${item}`}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
{story.error && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.8em",
|
||||
color: "#ff7b72",
|
||||
marginBottom: "4px",
|
||||
}}
|
||||
data-testid={`story-error-${story.storyId}`}
|
||||
>
|
||||
{story.error}
|
||||
</div>
|
||||
)}
|
||||
{story.items.length > 0 && (
|
||||
<ul
|
||||
style={{
|
||||
margin: "0 0 0 16px",
|
||||
padding: 0,
|
||||
fontSize: "0.85em",
|
||||
color: "#ccc",
|
||||
}}
|
||||
>
|
||||
{story.items.map((item) => (
|
||||
<li key={`todo-${story.storyId}-${item}`}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -43,8 +43,8 @@ describe("UpcomingPanel", () => {
|
||||
|
||||
it("renders story list with names", () => {
|
||||
const stories: UpcomingStory[] = [
|
||||
{ story_id: "31_view_upcoming", name: "View Upcoming Stories" },
|
||||
{ story_id: "32_worktree", name: "Worktree Orchestration" },
|
||||
{ story_id: "31_view_upcoming", name: "View Upcoming Stories", error: null },
|
||||
{ story_id: "32_worktree", name: "Worktree Orchestration", error: null },
|
||||
];
|
||||
render(<UpcomingPanel {...baseProps} stories={stories} />);
|
||||
|
||||
@@ -55,7 +55,9 @@ describe("UpcomingPanel", () => {
|
||||
});
|
||||
|
||||
it("renders story without name using story_id", () => {
|
||||
const stories: UpcomingStory[] = [{ story_id: "33_no_name", name: null }];
|
||||
const stories: UpcomingStory[] = [
|
||||
{ story_id: "33_no_name", name: null, error: null },
|
||||
];
|
||||
render(<UpcomingPanel {...baseProps} stories={stories} />);
|
||||
|
||||
expect(screen.getByText("33_no_name")).toBeInTheDocument();
|
||||
|
||||
@@ -146,20 +146,41 @@ export function UpcomingPanel({
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
|
||||
{story.name ?? story.story_id}
|
||||
</div>
|
||||
{story.name && (
|
||||
<div style={{ flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.75em",
|
||||
color: "#777",
|
||||
fontFamily: "monospace",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "8px",
|
||||
}}
|
||||
>
|
||||
{story.story_id}
|
||||
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
|
||||
{story.name ?? story.story_id}
|
||||
</div>
|
||||
{story.name && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.75em",
|
||||
color: "#777",
|
||||
fontFamily: "monospace",
|
||||
}}
|
||||
>
|
||||
{story.story_id}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{story.error && (
|
||||
<div
|
||||
style={{
|
||||
fontSize: "0.8em",
|
||||
color: "#ff7b72",
|
||||
marginTop: "4px",
|
||||
}}
|
||||
>
|
||||
{story.error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -98,6 +98,7 @@ test.describe("Story TODOs panel", () => {
|
||||
"The UI lists unchecked acceptance criteria.",
|
||||
"Each TODO is displayed as its full text.",
|
||||
],
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -124,6 +125,7 @@ test.describe("Story TODOs panel", () => {
|
||||
story_id: "28_ui_show_test_todos",
|
||||
story_name: "Show Remaining Test TODOs in the UI",
|
||||
todos: [],
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -137,6 +139,26 @@ test.describe("Story TODOs panel", () => {
|
||||
await expect(page.getByText("0 remaining")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows per-story front matter error", async ({ page }) => {
|
||||
await mockChatApis(page, {
|
||||
todos: {
|
||||
stories: [
|
||||
{
|
||||
story_id: "28_ui_show_test_todos",
|
||||
story_name: null,
|
||||
todos: [],
|
||||
error: "Missing front matter",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await openProject(page);
|
||||
|
||||
await expect(page.getByText("Missing front matter")).toBeVisible();
|
||||
await expect(page.getByText("28_ui_show_test_todos")).toBeVisible();
|
||||
});
|
||||
|
||||
test("shows TODO items from multiple stories", async ({ page }) => {
|
||||
await mockChatApis(page, {
|
||||
todos: {
|
||||
@@ -145,11 +167,13 @@ test.describe("Story TODOs panel", () => {
|
||||
story_id: "28_ui_show_test_todos",
|
||||
story_name: "Show TODOs",
|
||||
todos: ["First criterion."],
|
||||
error: null,
|
||||
},
|
||||
{
|
||||
story_id: "29_another_story",
|
||||
story_name: "Another Story",
|
||||
todos: ["Second criterion."],
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -19,6 +19,15 @@ pub enum StoryMetaError {
|
||||
InvalidFrontMatter(String),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for StoryMetaError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
StoryMetaError::MissingFrontMatter => write!(f, "Missing front matter"),
|
||||
StoryMetaError::InvalidFrontMatter(msg) => write!(f, "Invalid front matter: {msg}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct FrontMatter {
|
||||
name: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user