Finishing agent merge

This commit is contained in:
Dave
2026-02-19 18:05:21 +00:00
parent c94b3d4450
commit 8c2dc9b6a0
8 changed files with 186 additions and 29 deletions

View File

@@ -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,
},
],
});

View File

@@ -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());

View 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();
});
});

View File

@@ -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>

View File

@@ -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();

View File

@@ -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>