Add GET /workflow/upcoming endpoint that reads .story_kit/stories/upcoming/ and returns story IDs with names parsed from frontmatter. Add UpcomingPanel component wired into Chat view with loading, error, empty, and list states. 12 new tests (3 backend, 9 frontend) all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
136 lines
3.4 KiB
TypeScript
136 lines
3.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { workflowApi } from "./workflow";
|
|
|
|
const mockFetch = vi.fn();
|
|
|
|
beforeEach(() => {
|
|
vi.stubGlobal("fetch", mockFetch);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function okResponse(body: unknown) {
|
|
return new Response(JSON.stringify(body), {
|
|
status: 200,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
function errorResponse(status: number, text: string) {
|
|
return new Response(text, { status });
|
|
}
|
|
|
|
describe("workflowApi", () => {
|
|
describe("recordTests", () => {
|
|
it("sends POST to /workflow/tests/record", async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse(true));
|
|
|
|
const payload = {
|
|
story_id: "story-29",
|
|
unit: [{ name: "t1", status: "pass" as const }],
|
|
integration: [],
|
|
};
|
|
|
|
await workflowApi.recordTests(payload);
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
"/api/workflow/tests/record",
|
|
expect.objectContaining({ method: "POST" }),
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getAcceptance", () => {
|
|
it("sends POST and returns acceptance response", async () => {
|
|
const response = {
|
|
can_accept: true,
|
|
reasons: [],
|
|
warning: null,
|
|
summary: { total: 2, passed: 2, failed: 0 },
|
|
missing_categories: [],
|
|
};
|
|
mockFetch.mockResolvedValueOnce(okResponse(response));
|
|
|
|
const result = await workflowApi.getAcceptance({
|
|
story_id: "story-29",
|
|
});
|
|
|
|
expect(result.can_accept).toBe(true);
|
|
expect(result.summary.total).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe("getReviewQueueAll", () => {
|
|
it("sends GET to /workflow/review/all", async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse({ stories: [] }));
|
|
|
|
const result = await workflowApi.getReviewQueueAll();
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
"/api/workflow/review/all",
|
|
expect.objectContaining({}),
|
|
);
|
|
expect(result.stories).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe("ensureAcceptance", () => {
|
|
it("returns true when acceptance passes", async () => {
|
|
mockFetch.mockResolvedValueOnce(okResponse(true));
|
|
|
|
const result = await workflowApi.ensureAcceptance({
|
|
story_id: "story-29",
|
|
});
|
|
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("throws on error response", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
errorResponse(400, "Acceptance is blocked"),
|
|
);
|
|
|
|
await expect(
|
|
workflowApi.ensureAcceptance({ story_id: "story-29" }),
|
|
).rejects.toThrow("Acceptance is blocked");
|
|
});
|
|
});
|
|
|
|
describe("getUpcomingStories", () => {
|
|
it("sends GET to /workflow/upcoming", async () => {
|
|
const response = {
|
|
stories: [
|
|
{ story_id: "31_view_upcoming", name: "View Upcoming" },
|
|
{ story_id: "32_worktree", name: null },
|
|
],
|
|
};
|
|
mockFetch.mockResolvedValueOnce(okResponse(response));
|
|
|
|
const result = await workflowApi.getUpcomingStories();
|
|
|
|
expect(mockFetch).toHaveBeenCalledWith(
|
|
"/api/workflow/upcoming",
|
|
expect.objectContaining({}),
|
|
);
|
|
expect(result.stories).toHaveLength(2);
|
|
expect(result.stories[0].name).toBe("View Upcoming");
|
|
expect(result.stories[1].name).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("getReviewQueue", () => {
|
|
it("sends GET to /workflow/review", async () => {
|
|
mockFetch.mockResolvedValueOnce(
|
|
okResponse({ stories: [{ story_id: "s1", can_accept: true }] }),
|
|
);
|
|
|
|
const result = await workflowApi.getReviewQueue();
|
|
|
|
expect(result.stories).toHaveLength(1);
|
|
expect(result.stories[0].story_id).toBe("s1");
|
|
});
|
|
});
|
|
});
|