Merge story-29: Backfill tests for maximum coverage

Adds 57 Rust tests and 60 frontend tests across 4 batches:
- Batch 1: store, search, workflow
- Batch 2: fs, shell, http/workflow
- Batch 3: usePathCompletion, api/client, api/workflow
- Batch 4: App, GatePanel, ReviewPanel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dave
2026-02-19 14:46:42 +00:00
13 changed files with 1648 additions and 2 deletions

View File

@@ -0,0 +1,138 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { api } from "./client";
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("api client", () => {
describe("getCurrentProject", () => {
it("sends GET to /project", async () => {
mockFetch.mockResolvedValueOnce(okResponse("/home/user/project"));
const result = await api.getCurrentProject();
expect(mockFetch).toHaveBeenCalledWith(
"/api/project",
expect.objectContaining({}),
);
expect(result).toBe("/home/user/project");
});
it("returns null when no project open", async () => {
mockFetch.mockResolvedValueOnce(okResponse(null));
const result = await api.getCurrentProject();
expect(result).toBeNull();
});
});
describe("openProject", () => {
it("sends POST with path", async () => {
mockFetch.mockResolvedValueOnce(okResponse("/home/user/project"));
await api.openProject("/home/user/project");
expect(mockFetch).toHaveBeenCalledWith(
"/api/project",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ path: "/home/user/project" }),
}),
);
});
});
describe("closeProject", () => {
it("sends DELETE to /project", async () => {
mockFetch.mockResolvedValueOnce(okResponse(true));
await api.closeProject();
expect(mockFetch).toHaveBeenCalledWith(
"/api/project",
expect.objectContaining({ method: "DELETE" }),
);
});
});
describe("getKnownProjects", () => {
it("returns array of project paths", async () => {
mockFetch.mockResolvedValueOnce(okResponse(["/a", "/b"]));
const result = await api.getKnownProjects();
expect(result).toEqual(["/a", "/b"]);
});
});
describe("error handling", () => {
it("throws on non-ok response with body text", async () => {
mockFetch.mockResolvedValueOnce(errorResponse(404, "Not found"));
await expect(api.getCurrentProject()).rejects.toThrow("Not found");
});
it("throws with status code when no body", async () => {
mockFetch.mockResolvedValueOnce(errorResponse(500, ""));
await expect(api.getCurrentProject()).rejects.toThrow(
"Request failed (500)",
);
});
});
describe("searchFiles", () => {
it("sends POST with query", async () => {
mockFetch.mockResolvedValueOnce(
okResponse([{ path: "src/main.rs", matches: 1 }]),
);
const result = await api.searchFiles("hello");
expect(mockFetch).toHaveBeenCalledWith(
"/api/fs/search",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ query: "hello" }),
}),
);
expect(result).toHaveLength(1);
});
});
describe("execShell", () => {
it("sends POST with command and args", async () => {
mockFetch.mockResolvedValueOnce(
okResponse({ stdout: "output", stderr: "", exit_code: 0 }),
);
const result = await api.execShell("ls", ["-la"]);
expect(mockFetch).toHaveBeenCalledWith(
"/api/shell/exec",
expect.objectContaining({
method: "POST",
body: JSON.stringify({ command: "ls", args: ["-la"] }),
}),
);
expect(result.exit_code).toBe(0);
});
});
});

View File

@@ -0,0 +1,113 @@
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("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");
});
});
});