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 () => {
|
it("fetches upcoming stories on mount and renders panel", async () => {
|
||||||
mockedWorkflow.getUpcomingStories.mockResolvedValueOnce({
|
mockedWorkflow.getUpcomingStories.mockResolvedValueOnce({
|
||||||
stories: [
|
stories: [
|
||||||
{ story_id: "31_view_upcoming", name: "View Upcoming Stories" },
|
{ story_id: "31_view_upcoming", name: "View Upcoming Stories", error: null },
|
||||||
{ story_id: "32_worktree", name: null },
|
{ story_id: "32_worktree", name: null, error: null },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -544,6 +544,7 @@ describe("Chat review panel", () => {
|
|||||||
"The UI lists unchecked acceptance criteria.",
|
"The UI lists unchecked acceptance criteria.",
|
||||||
"Each TODO is displayed as its full text.",
|
"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_id: "28_ui_show_test_todos",
|
||||||
story_name: "Show Remaining Test TODOs in the UI",
|
story_name: "Show Remaining Test TODOs in the UI",
|
||||||
todos: [],
|
todos: [],
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
const [isCollectingCoverage, setIsCollectingCoverage] = useState(false);
|
const [isCollectingCoverage, setIsCollectingCoverage] = useState(false);
|
||||||
const [coverageError, setCoverageError] = useState<string | null>(null);
|
const [coverageError, setCoverageError] = useState<string | null>(null);
|
||||||
const [storyTodos, setStoryTodos] = useState<
|
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 [todoError, setTodoError] = useState<string | null>(null);
|
||||||
const [isTodoLoading, setIsTodoLoading] = useState(false);
|
const [isTodoLoading, setIsTodoLoading] = useState(false);
|
||||||
@@ -284,6 +289,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
storyId: s.story_id,
|
storyId: s.story_id,
|
||||||
storyName: s.story_name,
|
storyName: s.story_name,
|
||||||
items: s.todos,
|
items: s.todos,
|
||||||
|
error: s.error ?? null,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
setLastTodoRefresh(new Date());
|
setLastTodoRefresh(new Date());
|
||||||
@@ -319,6 +325,7 @@ export function Chat({ projectPath, onCloseProject }: ChatProps) {
|
|||||||
storyId: s.story_id,
|
storyId: s.story_id,
|
||||||
storyName: s.story_name,
|
storyName: s.story_name,
|
||||||
items: s.todos,
|
items: s.todos,
|
||||||
|
error: s.error ?? null,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
setLastTodoRefresh(new Date());
|
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;
|
storyId: string;
|
||||||
storyName: string | null;
|
storyName: string | null;
|
||||||
items: string[];
|
items: string[];
|
||||||
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TodoPanelProps {
|
interface TodoPanelProps {
|
||||||
@@ -29,6 +30,7 @@ export function TodoPanel({
|
|||||||
onRefresh,
|
onRefresh,
|
||||||
}: TodoPanelProps) {
|
}: TodoPanelProps) {
|
||||||
const totalTodos = todos.reduce((sum, s) => sum + s.items.length, 0);
|
const totalTodos = todos.reduce((sum, s) => sum + s.items.length, 0);
|
||||||
|
const hasErrors = todos.some((s) => s.error);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -127,7 +129,7 @@ export function TodoPanel({
|
|||||||
Retry
|
Retry
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : totalTodos === 0 ? (
|
) : totalTodos === 0 && !hasErrors ? (
|
||||||
<div style={{ fontSize: "0.85em", color: "#7ee787" }}>
|
<div style={{ fontSize: "0.85em", color: "#7ee787" }}>
|
||||||
All acceptance criteria complete.
|
All acceptance criteria complete.
|
||||||
</div>
|
</div>
|
||||||
@@ -140,7 +142,7 @@ export function TodoPanel({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{todos
|
{todos
|
||||||
.filter((s) => s.items.length > 0)
|
.filter((s) => s.items.length > 0 || s.error)
|
||||||
.map((story) => (
|
.map((story) => (
|
||||||
<div key={story.storyId}>
|
<div key={story.storyId}>
|
||||||
<div
|
<div
|
||||||
@@ -152,18 +154,32 @@ export function TodoPanel({
|
|||||||
>
|
>
|
||||||
{story.storyName ?? story.storyId}
|
{story.storyName ?? story.storyId}
|
||||||
</div>
|
</div>
|
||||||
<ul
|
{story.error && (
|
||||||
style={{
|
<div
|
||||||
margin: "0 0 0 16px",
|
style={{
|
||||||
padding: 0,
|
fontSize: "0.8em",
|
||||||
fontSize: "0.85em",
|
color: "#ff7b72",
|
||||||
color: "#ccc",
|
marginBottom: "4px",
|
||||||
}}
|
}}
|
||||||
>
|
data-testid={`story-error-${story.storyId}`}
|
||||||
{story.items.map((item) => (
|
>
|
||||||
<li key={`todo-${story.storyId}-${item}`}>{item}</li>
|
{story.error}
|
||||||
))}
|
</div>
|
||||||
</ul>
|
)}
|
||||||
|
{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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ describe("UpcomingPanel", () => {
|
|||||||
|
|
||||||
it("renders story list with names", () => {
|
it("renders story list with names", () => {
|
||||||
const stories: UpcomingStory[] = [
|
const stories: UpcomingStory[] = [
|
||||||
{ story_id: "31_view_upcoming", name: "View Upcoming Stories" },
|
{ story_id: "31_view_upcoming", name: "View Upcoming Stories", error: null },
|
||||||
{ story_id: "32_worktree", name: "Worktree Orchestration" },
|
{ story_id: "32_worktree", name: "Worktree Orchestration", error: null },
|
||||||
];
|
];
|
||||||
render(<UpcomingPanel {...baseProps} stories={stories} />);
|
render(<UpcomingPanel {...baseProps} stories={stories} />);
|
||||||
|
|
||||||
@@ -55,7 +55,9 @@ describe("UpcomingPanel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders story without name using story_id", () => {
|
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} />);
|
render(<UpcomingPanel {...baseProps} stories={stories} />);
|
||||||
|
|
||||||
expect(screen.getByText("33_no_name")).toBeInTheDocument();
|
expect(screen.getByText("33_no_name")).toBeInTheDocument();
|
||||||
|
|||||||
@@ -146,20 +146,41 @@ export function UpcomingPanel({
|
|||||||
gap: "8px",
|
gap: "8px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontWeight: 600, fontSize: "0.9em" }}>
|
<div style={{ flex: 1 }}>
|
||||||
{story.name ?? story.story_id}
|
|
||||||
</div>
|
|
||||||
{story.name && (
|
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
fontSize: "0.75em",
|
display: "flex",
|
||||||
color: "#777",
|
alignItems: "center",
|
||||||
fontFamily: "monospace",
|
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>
|
</div>
|
||||||
)}
|
{story.error && (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
fontSize: "0.8em",
|
||||||
|
color: "#ff7b72",
|
||||||
|
marginTop: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{story.error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ test.describe("Story TODOs panel", () => {
|
|||||||
"The UI lists unchecked acceptance criteria.",
|
"The UI lists unchecked acceptance criteria.",
|
||||||
"Each TODO is displayed as its full text.",
|
"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_id: "28_ui_show_test_todos",
|
||||||
story_name: "Show Remaining Test TODOs in the UI",
|
story_name: "Show Remaining Test TODOs in the UI",
|
||||||
todos: [],
|
todos: [],
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -137,6 +139,26 @@ test.describe("Story TODOs panel", () => {
|
|||||||
await expect(page.getByText("0 remaining")).toBeVisible();
|
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 }) => {
|
test("shows TODO items from multiple stories", async ({ page }) => {
|
||||||
await mockChatApis(page, {
|
await mockChatApis(page, {
|
||||||
todos: {
|
todos: {
|
||||||
@@ -145,11 +167,13 @@ test.describe("Story TODOs panel", () => {
|
|||||||
story_id: "28_ui_show_test_todos",
|
story_id: "28_ui_show_test_todos",
|
||||||
story_name: "Show TODOs",
|
story_name: "Show TODOs",
|
||||||
todos: ["First criterion."],
|
todos: ["First criterion."],
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
story_id: "29_another_story",
|
story_id: "29_another_story",
|
||||||
story_name: "Another Story",
|
story_name: "Another Story",
|
||||||
todos: ["Second criterion."],
|
todos: ["Second criterion."],
|
||||||
|
error: null,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,15 @@ pub enum StoryMetaError {
|
|||||||
InvalidFrontMatter(String),
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct FrontMatter {
|
struct FrontMatter {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
|
|||||||
Reference in New Issue
Block a user